Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

COBOL  Module 6 – Tables: OCCURS, SEARCH and SORT

SEARCH ALL, table SORT and OCCURS DEPENDING ON

Search COBOL tables with SEARCH and SEARCH ALL, keep them in key order with the table SORT, and size variable-length tables with OCCURS DEPENDING ON safely.

  • Intermediate
  • 20 minutes
  • Examples run with GnuCOBOL 3.2 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Search tables with SEARCH and SEARCH ALL and state the precondition of each
  • Sort a table in memory with the table SORT statement
  • Use OCCURS DEPENDING ON for variable-length tables and validate its counter
  • Compare the cost of serial and binary searches from measured runs

Before you start

What you’ll learn · Before you start

What you will learn

  • Search tables with SEARCH and SEARCH ALL and state the precondition of each
  • Sort a table in memory with the table SORT statement
  • Use OCCURS DEPENDING ON for variable-length tables and validate its counter
  • Compare the cost of serial and binary searches from measured runs

Before you start

On this page

Two slabs appended to the end of a rate table were enough to make SEARCH ALL report 104 and 162 as not found, although both codes were in the table, and nothing raised an error. A binary search is fast because it trusts the table to be in key order, and that trust has to be earned before every search.

Below you will search tables with SEARCH and SEARCH ALL and meet the condition each one depends on, put a table in order with the table SORT, measure what each search costs as a table grows, and size a table to its data with OCCURS DEPENDING ON without trusting a count that came from outside.

SEARCH reads the table from where its index stands

A serial SEARCH tests its WHEN condition entry by entry and moves the table’s index on after each miss. IBM’s reference says it runs “beginning at the current setting of the search index”, and that before a serial search “you must set the value” of that index. When the index passes the last entry, AT END runs. This rate table holds eight slabs in ascending code order, with a teaching rate for each:

search.cob: SEARCH and SEARCH ALL on a rate table COB · search.cob
       IDENTIFICATION DIVISION.
       PROGRAM-ID. SEARCH1.
      * A rate table of eight slabs in ascending code order, searched
      * with SEARCH, once with a stale index, and with SEARCH ALL.
      * The rates are a teaching example.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  RATE-VALUES.
           05  FILLER   PIC X(7) VALUE "1040650".
           05  FILLER   PIC X(7) VALUE "1170675".
           05  FILLER   PIC X(7) VALUE "1250700".
           05  FILLER   PIC X(7) VALUE "1330710".
           05  FILLER   PIC X(7) VALUE "1480725".
           05  FILLER   PIC X(7) VALUE "1560740".
           05  FILLER   PIC X(7) VALUE "1620750".
           05  FILLER   PIC X(7) VALUE "1790775".
       01  RATE-TABLE REDEFINES RATE-VALUES.
           05  RATE-ENTRY OCCURS 8 TIMES
                   ASCENDING KEY IS RT-CODE
                   INDEXED BY RT-IDX.
               10  RT-CODE  PIC 9(3).
               10  RT-RATE  PIC 9V999.
       01  WANTED       PIC 9(3).
       01  ENTRY-NO     PIC 99.
       PROCEDURE DIVISION.
           MOVE 162 TO WANTED
           SET RT-IDX TO 1
           PERFORM SERIAL-FIND
           MOVE 117 TO WANTED
           PERFORM SERIAL-FIND
           SET RT-IDX TO 1
           PERFORM SERIAL-FIND
           MOVE 162 TO WANTED
           PERFORM BINARY-FIND
           MOVE 140 TO WANTED
           PERFORM BINARY-FIND
           STOP RUN.
       SERIAL-FIND.
           SEARCH RATE-ENTRY
               AT END
                   DISPLAY "SEARCH     " WANTED ": not found"
               WHEN RT-CODE(RT-IDX) = WANTED
                   SET ENTRY-NO TO RT-IDX
                   DISPLAY "SEARCH     " WANTED ": entry " ENTRY-NO
                       ", rate " RT-RATE(RT-IDX)
           END-SEARCH.
       BINARY-FIND.
           SEARCH ALL RATE-ENTRY
               AT END
                   DISPLAY "SEARCH ALL " WANTED ": not found"
               WHEN RT-CODE(RT-IDX) = WANTED
                   SET ENTRY-NO TO RT-IDX
                   DISPLAY "SEARCH ALL " WANTED ": entry " ENTRY-NO
                       ", rate " RT-RATE(RT-IDX)
           END-SEARCH.
Five searches, one of them with a stale index Python · search_session.py
from session import files, run

# A serial SEARCH from entry 1, again from where the index was left, and after a SET; then SEARCH ALL for a code that
# is in the table and one that is not.
files("search.cob")
run("""
cobc -x search.cob && ./search
""")

Output

$ cobc -x search.cob && ./search
SEARCH     162: entry 07, rate 0.750
SEARCH     117: not found
SEARCH     117: entry 02, rate 0.675
SEARCH ALL 162: entry 07, rate 0.750
SEARCH ALL 140: not found

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 search_session.py

The first SEARCH started at entry 1 and found 162 at entry 7. Step through it: each step is one entry the search tests, the way SEARCH moves RT-IDX through the table:

Interactive · step through it A serial search for 162 in the eight slab codes Check each value from the left until the target turns up or the array ends.
Array of 8: 104 117 125 133 148 156 162 179.10401171125213331484156516261797
  • being compared or looked at
  • in its final place
  • visited or ruled out

Pseudo-code

  1. for i from 0 to n − 1:
  2. if a[i] = target: return i
  3. return "not found"

Look for 162 by checking each value from the left until it turns up or the array ends.

When it finishes: Found 162 at index 6.

Values checked 0

The figure numbers its positions from 0, as most programming languages do, so its position 6 is entry 7 of the COBOL table. The second search, for 117, printed not found, although 117 is entry 2. It ran without a SET, so it started where the first search had left RT-IDX, at entry 7:

Interactive · stage by stage A serial SEARCH that starts where the last one stopped Step through the search for 117 without and then with SET RT-IDX TO 1.
A serial SEARCH that starts where the last one stopped, step 6 of 6. Showing RT-IDX after the search, Entry 7: 162, Entry 8: 179, Past entry 8, SET RT-IDX TO 1, Entry 2: 117. In focus: Entry 2: 117.the fixRT-IDX after the searchentry 7, code 162Entry 7: 162is it 117?Entry 8: 179is it 117?Past entry 8AT END runsSET RT-IDX TO 1back to the first entryEntry 2: 117WHEN is true

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Where the index stands. The first SEARCH found 162 at entry 7 and left RT-IDX there. Nothing set it back, and IBM's reference says a serial SEARCH begins at the current setting of the search index.
  2. Entry 7. The search for 117 starts at entry 7. Its code is 162, not 117, so SEARCH moves the index on to entry 8.
  3. Entry 8. Entry 8 holds 179, not 117 either. The index moves past the last entry.
  4. Not found. The index is past the highest occurrence, so the search ends and AT END prints 117 not found, although 117 is entry 2 of the table.
  5. The reset. SET RT-IDX TO 1 puts the index on the first entry again, so the next SEARCH reads the table from the start.
  6. Found. Entry 1 holds 104, then entry 2 holds 117, so the WHEN is true and the session printed entry 02, rate 0.675.

A SEARCH without SET

Wrong: SEARCH RATE-ENTRY in a paragraph that is performed for many codes, with SET RT-IDX TO 1 only once at the start of the program. Right: SET RT-IDX TO 1 immediately before every serial SEARCH, in the same paragraph.

SEARCH ALL halves a table that is in key order

SEARCH ALL is a binary search: A search that compares the key with the middle entry of the range that can still hold it and keeps one half, so n entries need at most the whole part of log2(n) plus 1 probes. It needs the entries in key order.. It needs the table described with ASCENDING KEY: The phrase of an OCCURS clause that names the field a table is kept in order by, smallest first. SEARCH ALL relies on that order, and the table SORT can produce it. or DESCENDING KEY and INDEXED BY, sets the index itself, and looks at the middle of the range that can still hold the key, so each probe rules out half of what is left. IBM’s reference adds the condition: the results are “predictable only when” the data “is ordered in ASCENDING KEY or DESCENDING KEY order”. In search.cob, SEARCH ALL found 162 at entry 7 and reported 140, which is not in the table, as not found. Step through its probes on the same eight codes:

Interactive · step through it A binary search for 162 in the eight slab codes Each probe halves the range where the target can still be; the dimmed values are ruled out.
Array of 8: 104 117 125 133 148 156 162 179. lo at 0, hi at 7.10401171125213331484156516261797lohi
  • being compared or looked at
  • in its final place
  • visited or ruled out

Pseudo-code

  1. lo ← 0; hi ← n − 1
  2. while lo ≤ hi:
  3. mid ← lo + ⌊(hi − lo) / 2⌋
  4. if a[mid] = target: return mid
  5. if a[mid] < target: lo ← mid + 1
  6. else: hi ← mid − 1
  7. return "not found"

Look for 162. The invariant: if 162 is in the array, it is somewhere in a[lo..hi]. At the start that range is the whole array, positions 0 to 7.

When it finishes: Found 162 at index 6.

Probes 0
Positions left 8

The key order is a promise the program makes, and nothing checks it. unsorted.cob keeps the same eight slabs but has two of them appended out of order, 162 and 104 after 179, as an update that adds new slabs at the end might leave them:

unsorted.cob: SEARCH ALL before and after a table SORT COB · unsorted.cob
       IDENTIFICATION DIVISION.
       PROGRAM-ID. UNSORTED.
      * The rate table with two slabs appended out of order, as an
      * update might leave it. SEARCH ALL assumes ascending codes.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  RATE-VALUES.
           05  FILLER   PIC X(7) VALUE "1170675".
           05  FILLER   PIC X(7) VALUE "1250700".
           05  FILLER   PIC X(7) VALUE "1330710".
           05  FILLER   PIC X(7) VALUE "1480725".
           05  FILLER   PIC X(7) VALUE "1560740".
           05  FILLER   PIC X(7) VALUE "1790775".
           05  FILLER   PIC X(7) VALUE "1620750".
           05  FILLER   PIC X(7) VALUE "1040650".
       01  RATE-TABLE REDEFINES RATE-VALUES.
           05  RATE-ENTRY OCCURS 8 TIMES
                   ASCENDING KEY IS RT-CODE
                   INDEXED BY RT-IDX.
               10  RT-CODE  PIC 9(3).
               10  RT-RATE  PIC 9V999.
       01  WANTED       PIC 9(3).
       PROCEDURE DIVISION.
           MOVE 104 TO WANTED
           PERFORM BINARY-FIND
           MOVE 162 TO WANTED
           PERFORM BINARY-FIND
           MOVE 133 TO WANTED
           PERFORM BINARY-FIND
           SORT RATE-ENTRY ON ASCENDING KEY RT-CODE
           DISPLAY "after SORT, entry 1 is " RT-CODE(1)
               " and entry 8 is " RT-CODE(8)
           MOVE 104 TO WANTED
           PERFORM BINARY-FIND
           MOVE 162 TO WANTED
           PERFORM BINARY-FIND
           STOP RUN.
       BINARY-FIND.
           SEARCH ALL RATE-ENTRY
               AT END
                   DISPLAY "SEARCH ALL " WANTED ": not found"
               WHEN RT-CODE(RT-IDX) = WANTED
                   DISPLAY "SEARCH ALL " WANTED ": rate "
                       RT-RATE(RT-IDX)
           END-SEARCH.
Searches on the unsorted table, then after SORT Python · unsorted_session.py
from session import files, run

# SEARCH ALL on a table whose last two entries are out of order, then the same searches after a table SORT.
files("unsorted.cob")
run("""
cobc -x unsorted.cob && ./unsorted
""")

Output

$ cobc -x unsorted.cob && ./unsorted
SEARCH ALL 104: not found
SEARCH ALL 162: not found
SEARCH ALL 133: rate 0.710
after SORT, entry 1 is 104 and entry 8 is 179
SEARCH ALL 104: rate 0.650
SEARCH ALL 162: rate 0.750

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 unsorted_session.py

SEARCH ALL reported 104 and 162 as not found, while 133, in the part that was still in order, was found. The binary search ruled out the half where each misplaced code really was, because by key order it could not have been there.

Key idea

SEARCH ALL is correct only on a table in the order its KEY clause declares; on any other table it can miss a key that is present, without an error.

Checkpoint

Check: Serial SEARCH and its index · SEARCH ALL and key order

  1. Question 1 of 2 A serial SEARCH found 162 at entry 7. The next SEARCH, for 117, runs without a SET. What happens?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It starts at entry 7, passes the end of the table and reports 117 as not found

    The session printed SEARCH 117 not found, then found it at entry 02 after SET RT-IDX TO 1. Put the SET right before every serial SEARCH.

  2. Question 2 of 2 In unsorted.cob, 162 and 104 were appended after 179. What did SEARCH ALL report for 104?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Not found, although 104 was in the table

    IBM's reference says the results are predictable only when the table is in its declared key order. 133, in the ordered part, was still found, which is why such a fault can pass a test that tries only one code.

SORT puts a table in key order, in place

The second half of unsorted.cob shows the fix. SORT RATE-ENTRY ON ASCENDING KEY RT-CODE is the table form of the SORT statement. IBM’s reference says it “sorts table elements according to specified table keys”, and that the sorted elements “are placed in the same table”. After it, entry 1 was 104 and entry 8 was 179, and SEARCH ALL found both codes it had missed.

When the SORT names no key, the KEY phrase of the table’s OCCURS clause decides, so the sort and the searches cannot disagree about the order. Where codes can be equal, WITH DUPLICATES keeps equal entries in their original order; IBM’s reference says that without it, their relative order “is undefined”.

Sort once, after loading

Sort a table once, right after it is loaded or changed, and before the first SEARCH ALL, rather than before every search. A load from a file that is supposed to be in order can also check the order as it stores each entry, and fail when a code is smaller than the one before it.

What the two searches cost, measured

A serial search of a table of n entries tests every entry before it can report a miss, and about half of them, on average, for a key that is there. A binary search halves the range at every probe, so it needs at most ⌊log₂ n⌋ + 1 probes: 10 for 1,000 entries, 13 for 5,000 and 15 for 20,000. Step the chart through n doubling, with the lesson’s sizes among the steps, and switch the target between a missing key and every key that is present:

Interactive · watch it grow Entries tested and probes made, as the table grows to 20,000 n doubles at each step, from 1 up to 20,000, with 1,000 and 5,000 as steps too. Each mark counts what the algorithm did at that size; the dashed line is the count worked out by reasoning, drawn in full from the start.
Entries tested and probes made, as the table grows to 20,000: at n = 1, linear search checks 1 value, binary search makes 1 probe; dashed lines show the derived counts.Linear search · values checked (log scale) · dashed: nBinary search · probes · dashed: ⌊log₂ n⌋ + 1110010k100k051015201101001k10kn, the size of the input (log scale: n doubles at each step)n = 1
  • Linear search, measured (circles)
  • Binary search, measured (squares)
  • Derived count (dashed): n for linear search; ⌊log₂ n⌋ + 1 for binary search
  • The size in focus (ringed)

n = 1: linear search checks 1 value and binary search makes 1 probe. The dashed lines are the derived counts: n and ⌊log₂ n⌋ + 1.

From n = 1 to 20,000, searching for a value larger than every one, the counts grew: linear search from 1 to 20,000 values checked and binary search from 1 to 15 probes. Each matched the derived count at every size: n for linear search and ⌊log₂ n⌋ + 1 for binary search.

Size n 1
Linear search · values checked 1
Binary search · probes 1
Scan ÷ binary search 1×
Every measured count as a table
Measured at each size, searching for a value larger than every one
nLinear search: values checkedBinary search: probes
111
222
443
884
16165
32326
64647
1281288
2562569
51251210
1,0001,00010
1,0241,02411
2,0482,04812
4,0964,09613
5,0005,00013
8,1928,19214
16,38416,38415
20,00020,00015

Counts are not times, so this program measures both searches. It makes 200,000 lookups, about half of them for keys that are not in the table, in tables of 1,000, 5,000 and 20,000 keys, built with cobc -O2:

searchbench.cob: 200,000 lookups with SEARCH or SEARCH ALL COB · searchbench.cob
       IDENTIFICATION DIVISION.
       PROGRAM-ID. SEARCHBENCH.
      * 200,000 lookups, half of them for keys that are not in the
      * table, with SEARCH or SEARCH ALL. The table holds N keys,
      * 2, 4, 6 and so on, N from the command line, at most 20000.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  TABLE-SIZE   PIC 9(5) VALUE 1.
       01  KEY-TABLE.
           05  KEY-ENTRY OCCURS 1 TO 20000 TIMES
                   DEPENDING ON TABLE-SIZE
                   ASCENDING KEY IS K-VALUE
                   INDEXED BY K-IDX.
               10  K-VALUE  PIC 9(6).
       01  ARGS         PIC X(20).
       01  MODE-ARG     PIC X(6).
       01  SIZE-ARG     PIC X(6).
       01  I            PIC 9(7) COMP-5.
       01  WANTED       PIC 9(6).
       01  FOUND-COUNT  PIC 9(6) VALUE 0.
       01  MISS-COUNT   PIC 9(6) VALUE 0.
       PROCEDURE DIVISION.
           ACCEPT ARGS FROM COMMAND-LINE
           UNSTRING ARGS DELIMITED BY SPACE INTO MODE-ARG SIZE-ARG
           END-UNSTRING
           COMPUTE TABLE-SIZE = FUNCTION NUMVAL(SIZE-ARG)
           PERFORM VARYING I FROM 1 BY 1 UNTIL I > TABLE-SIZE
               COMPUTE K-VALUE(I) = I * 2
           END-PERFORM
           PERFORM VARYING I FROM 1 BY 1 UNTIL I > 200000
               COMPUTE WANTED = FUNCTION MOD(I, 2 * TABLE-SIZE)
               IF MODE-ARG = "serial"
                   SET K-IDX TO 1
                   SEARCH KEY-ENTRY
                       AT END ADD 1 TO MISS-COUNT
                       WHEN K-VALUE(K-IDX) = WANTED
                           ADD 1 TO FOUND-COUNT
                   END-SEARCH
               ELSE
                   SEARCH ALL KEY-ENTRY
                       AT END ADD 1 TO MISS-COUNT
                       WHEN K-VALUE(K-IDX) = WANTED
                           ADD 1 TO FOUND-COUNT
                   END-SEARCH
               END-IF
           END-PERFORM
           DISPLAY MODE-ARG " " TABLE-SIZE " keys: found " FOUND-COUNT
               ", not found " MISS-COUNT
           STOP RUN.
User CPU time of each search at three table sizes Python · searchbench_session.py
from session import files, run

# The same 200,000 lookups with SEARCH and with SEARCH ALL in tables of 1000, 5000 and 20000 keys, built with -O2.
# Bash's time reports the user CPU time of each run.
files("searchbench.cob")
run("""
cobc -x -O2 searchbench.cob
TIMEFORMAT='  user %2U s'; for n in 1000 5000 20000; do time ./searchbench serial $n; time ./searchbench binary $n; done
""")

Output

$ cobc -x -O2 searchbench.cob
$ TIMEFORMAT='  user %2U s'; for n in 1000 5000 20000; do time ./searchbench serial $n; time ./searchbench binary $n; done
serial 01000 keys: found 099900, not found 100100
  user 0.15 s
binary 01000 keys: found 099900, not found 100100
  user 0.08 s
serial 05000 keys: found 099980, not found 100020
  user 0.50 s
binary 05000 keys: found 099980, not found 100020
  user 0.08 s
serial 20000 keys: found 099995, not found 100005
  user 1.85 s
binary 20000 keys: found 099995, not found 100005
  user 0.09 s

This output changes from run to run: CPU times depend on the computer and change a little from run to run; the found and not-found counts do not

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 searchbench_session.py

Both searches found the same keys at every size, which checks that they agree. On the arm64 Mac that ran them, under macOS 26, the serial searches took more time in step with the table, roughly three to four times as much each time it grew, while SEARCH ALL stayed near the same small figure at all three sizes. The ratio of the times is far smaller than the ratio of the counts on the chart, because each run also spends time on its loop, the MOD and the counters, which are the same for both searches and are most of what a binary search costs here.

Key idea

A serial SEARCH costs time in proportion to the table, a binary SEARCH ALL hardly any more as the table grows, so a large table that is searched often belongs in key order with SEARCH ALL.

Checkpoint

Check: The table SORT · What each search costs

  1. Question 1 of 2 Which statement made SEARCH ALL find 104 and 162 in unsorted.cob?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: SORT RATE-ENTRY ON ASCENDING KEY RT-CODE

    After the SORT, entry 1 was 104 and entry 8 was 179. Sort once after loading, before the first SEARCH ALL.

  2. Question 2 of 2 At most how many probes does a binary search make in a table of 5,000 keys?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 13 probes

    ⌊log₂ 5000⌋ + 1 = 12 + 1 = 13, since 2¹² = 4,096 is at most 5,000 and 2¹³ = 8,192 is more. A serial search tests all 5,000 keys before it reports a miss.

OCCURS DEPENDING ON makes a table as long as its counter

A statement record that holds as many amounts as the account had transactions does not need room for the largest case in every record. OCCURS 1 TO 10 TIMES DEPENDING ON TXN-COUNT describes such a variable-length table: A table declared with OCCURS min TO max TIMES DEPENDING ON a counter. The counter's current value is the number of entries, so it sets the length of the table and of its record.: IBM’s reference says the counter is “the data item whose current value represents the current number of occurrences”, and the record’s length follows it:

odo.cob: a record whose length follows TXN-COUNT COB · odo.cob
       IDENTIFICATION DIVISION.
       PROGRAM-ID. ODO.
      * A statement record with a variable number of amounts: up to
      * 10, as many as TXN-COUNT says. Its length follows the count.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  TXN-COUNT    PIC 99.
       01  STATEMENT-REC.
           05  ACCT-NO      PIC X(6) VALUE "AC0417".
           05  TXN-AMT      PIC 9(5) OCCURS 1 TO 10 TIMES
                   DEPENDING ON TXN-COUNT.
       01  REC-LEN      PIC 999.
       PROCEDURE DIVISION.
           MOVE 2 TO TXN-COUNT
           PERFORM SHOW-LENGTH
           MOVE 3 TO TXN-COUNT
           PERFORM SHOW-LENGTH
           MOVE 11 TO TXN-COUNT
           PERFORM SHOW-LENGTH
           PERFORM CHECK-COUNT
           STOP RUN.
       SHOW-LENGTH.
           COMPUTE REC-LEN = FUNCTION LENGTH(STATEMENT-REC)
           DISPLAY "TXN-COUNT " TXN-COUNT ": record of " REC-LEN
               " bytes".
       CHECK-COUNT.
           IF TXN-COUNT < 1 OR TXN-COUNT > 10
               DISPLAY "TXN-COUNT " TXN-COUNT ": rejected, the table"
                   " holds 1 to 10"
           END-IF.

The session also runs odocheck.cob, which the next section explains:

Three counts, then a write past the count in three builds Python · odo_session.py
from session import files, run

# The record's length for three counts, built with -debug, then a write past the count in three builds: normal, -debug,
# and -debug with the IBM dialect.
files("odo.cob", "odocheck.cob")
run("""
cobc -x -debug odo.cob && ./odo
cobc -x odocheck.cob && ./odocheck
!cobc -x -debug -o odocheck-debug odocheck.cob && ./odocheck-debug
cobc -x -debug -std=ibm -o odocheck-ibm odocheck.cob && ./odocheck-ibm
""")

Output

$ cobc -x -debug odo.cob && ./odo
TXN-COUNT 02: record of 016 bytes
TXN-COUNT 03: record of 021 bytes
TXN-COUNT 11: record of 061 bytes
TXN-COUNT 11: rejected, the table holds 1 to 10
$ cobc -x odocheck.cob && ./odocheck
wrote TXN-AMT(3) while TXN-COUNT is 2
$ cobc -x -debug -o odocheck-debug odocheck.cob && ./odocheck-debug
libcob: odocheck.cob:15: error: subscript of 'TXN-AMT' out of bounds: 3
note: current maximum subscript for 'TXN-AMT': 2

 Last statement of "ODOCHECK" was MOVE at line 15 of odocheck.cob
	ENTRY ODOCHECK at odocheck.cob:13
 Started by ./odocheck-debug
[exit status 1]
$ cobc -x -debug -std=ibm -o odocheck-ibm odocheck.cob && ./odocheck-ibm
wrote TXN-AMT(3) while TXN-COUNT is 2

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 odo_session.py

The record was 16 bytes with 2 amounts and 21 with 3: 6 bytes of account and 5 for each amount. Step through the three counts:

Interactive · step by step How long STATEMENT-REC is for three values of TXN-COUNT Step through the counts. The record is the boxes up to the one in focus.
How long STATEMENT-REC is for three values of TXN-COUNT, step 5 of 5. Showing ACCT-NO, TXN-AMT(1) and (2) (bytes 7-16: in the record), TXN-AMT(3) (bytes 17-21: in the record), TXN-AMT(4) to (10), A count of 11 (count 11: rejected). In focus: A count of 11.past OCCURS 10ACCT-NObytes 1-6TXN-AMT(1) and (2)bytes 7-16: in the recordTXN-AMT(3)bytes 17-21: in the recordTXN-AMT(4) to (10)bytes 22-56A count of 11count 11: rejected

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Count 2. With TXN-COUNT = 2 the record is ACCT-NO and two amounts, 6 + 2 x 5 = 16 bytes, which is what FUNCTION LENGTH printed.
  2. Count 3. One more amount makes 21 bytes. Nothing moves in storage; the record simply ends 5 bytes later, because its length is worked out from the count each time it is used.
  3. The largest record. OCCURS 1 TO 10 TIMES reserves room for ten amounts, so the area is 6 + 10 x 5 = 56 bytes. Any count from 1 to 10 fits.
  4. Count 11. With TXN-COUNT = 11, FUNCTION LENGTH printed 61 bytes, 5 more than the area holds, and even the -debug build gave no error. IBM's reference calls the behaviour undefined when the count is outside the range of the OCCURS.
  5. The check. CHECK-COUNT tested the count against 1 to 10 and rejected 11 before the record was used. A count read from input needs that test every time.

A count of 11 gave 61 bytes, 5 more than the 56 bytes the area can hold, and this program was built with -debug, which said nothing about it. IBM’s reference is plain about such a value: “the behavior is undefined if the value of the object is outside of the range”. CHECK-COUNT tested the count against 1 to 10 and rejected 11, which is the only check that ran.

Key idea

The counter of an OCCURS DEPENDING ON decides how long the table is, so test a count that comes from input against the OCCURS limits before anything uses the record.

Checkpoint

Check: OCCURS DEPENDING ON and its counter

  1. Question 1 of 2 STATEMENT-REC is a 6-byte ACCT-NO and TXN-AMT PIC 9(5) OCCURS 1 TO 10 TIMES DEPENDING ON TXN-COUNT. How many bytes is it when TXN-COUNT is 4?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 26 bytes

    6 bytes of account and 4 x 5 bytes of amounts make 26, between the 21 that 3 amounts gave and the 56 of the full area.

  2. Question 2 of 2 A record read from a file says TXN-COUNT = 11 for a table of OCCURS 1 TO 10. What does the program need?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Its own test of the count against 1 to 10 before the record is used

    IBM's reference calls the behaviour undefined when the count is outside the range of the OCCURS clause.

A write past the count is caught only in some builds

odocheck.cob writes TXN-AMT(3) while TXN-COUNT is 2, inside the area but past the current count. The normal build let it happen. The -debug build stopped with subscript of 'TXN-AMT' out of bounds: 3 and the note current maximum subscript for 'TXN-AMT': 2. The same -debug build with -std=ibm let it happen again.

GnuCOBOL 3.2’s NEWS explains the difference. The full check “which checks against the ODO item is still used in the default dialect, but several dialects now only check against the maximum only”: under the IBM dialect, any subscript up to 10 passes. A test build in the IBM dialect therefore needs the program’s own checks of the count as much as a production build does.

Items after a variable table

GnuCOBOL 3.2 also moved -fodoslide into the dialect settings: the NEWS says it “is now implied with -std=ibm/mvs/bs2000”. It decides where items that follow a variable table are placed, so programs with such records that are built with those dialects may need to be compiled again, or built with -fno-odoslide to keep the old layout.

Checkpoint

Check: Checks of a write past the count

  1. One question odocheck.cob writes TXN-AMT(3) while TXN-COUNT is 2. Which build stopped it?

    Choose one answer.

    Show the answer: One question

    Answer: Only the -debug build in the default dialect

    GnuCOBOL 3.2's NEWS says several dialects now check only against the maximum. Tests in the IBM dialect still need the program's own checks.

Practise: search, sort and size tables

The questions below come from the searches, sorts and counts of this lesson’s programs. Decide each answer from the code and the table’s order, then compare it with the session:

Practice that adapts to you

Practise this lesson

17 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 17 Where does a serial SEARCH begin?

    Choose one answer.

    Show the answer: Question 1 of 17

    Answer: At the current setting of the table's index

    SET the index to 1 before each serial SEARCH.

  2. Question 2 of 17 A serial SEARCH has no AT END phrase, and no entry matches. What happens?

    Choose one answer.

    Show the answer: Question 2 of 17

    Answer: Control passes to the next statement after the SEARCH, and nothing says that the key was missing

    Code AT END on every SEARCH, so that a missing code is reported instead of being treated like a match.

  3. Question 3 of 17 How many entries does the serial SEARCH test to find 162 in the eight codes, starting at entry 1?

    Type a number.

    Show the answer: Question 3 of 17

    Answer: 7 entries

    162 is entry 7, so the search tests entries 1 to 7, as the AlgoViz run shows; for a code that is missing it would test all 8.

  4. Question 4 of 17 What does SEARCH ALL need?

    Choose every answer that is right.

    Show the answer: Question 4 of 17

    Answer:

    • An INDEXED BY phrase on the table
    • An ASCENDING KEY or DESCENDING KEY phrase on the table
    • The entries really in the declared key order

    The first two are checked when the program is compiled. The third is not checked at all, which is why the order has to be guaranteed by the program.

  5. Question 5 of 17 SEARCH ALL ends in AT END for 140. What may the code assume about RT-IDX afterwards?

    Choose one answer.

    Show the answer: Question 5 of 17

    Answer: Nothing; IBM's reference says the final setting of the index is not predictable after a failed search

    Use the index only inside the WHEN branch, where it names the matching entry.

  6. Question 6 of 17 A test of a rate lookup searches only for 133 and passes. Why is that test too weak after a change to how the table is loaded?

    Choose one answer.

    Show the answer: Question 6 of 17

    Answer: An out-of-order table can still give the right answer for some keys, as 133 was found while 104 and 162 were missed

    A cheap guard is a check of the order right after loading: each key greater than the one before it.

  7. Question 7 of 17 What does SORT RATE-ENTRY ON ASCENDING KEY RT-CODE do?

    Choose one answer.

    Show the answer: Question 7 of 17

    Answer: It puts the entries of the table into ascending order of RT-CODE, in the same table

    After the SORT, unsorted.cob had 104 in entry 1 and 179 in entry 8.

  8. Question 8 of 17 SORT RATE-ENTRY is written with no KEY phrase. Which order does it use?

    Choose one answer.

    Show the answer: Question 8 of 17

    Answer: The KEY phrase of the table's OCCURS clause, ASCENDING KEY IS RT-CODE

    Letting the SORT use the table's KEY keeps the sort and SEARCH ALL in agreement about the order.

  9. Question 9 of 17 Two entries have the same code, and their original order matters. What must the SORT say?

    Choose one answer.

    Show the answer: Question 9 of 17

    Answer: WITH DUPLICATES, which keeps equal entries in their order before the sort

    Ties are common in rate tables that keep the history of a slab; WITH DUPLICATES makes their order defined.

  10. Question 10 of 17 How many entries does a serial SEARCH test before it reports a miss in a table of 20,000 keys?

    Type a number.

    Show the answer: Question 10 of 17

    Answer: 20000 entries

    It must test every entry to be sure the key is missing. A binary search needs at most 15 probes in the same table.

  11. Question 11 of 17 In the measured runs, SEARCH ALL took about the same time at 1,000, 5,000 and 20,000 keys. Why?

    Choose one answer.

    Show the answer: Question 11 of 17

    Answer: Its probes grew only from 10 to 15, so the loop around the searches, the same in every run, was most of the time

    The serial times grew with the table because its tests did; the binary search's cost was hidden by the loop's own fixed work.

  12. Question 12 of 17 A table of 30 codes is searched once per record of a large file, and it is loaded in no particular order. Which search fits?

    Choose one answer.

    Show the answer: Question 12 of 17

    Answer: Either works; a serial SEARCH is safe on unsorted data, and sorting first makes SEARCH ALL safe and faster

    For small tables the choice is about safety, not speed; for tables of thousands of entries searched often, sort and use SEARCH ALL, as the measured runs show.

  13. Question 13 of 17 In OCCURS 1 TO 10 TIMES DEPENDING ON TXN-COUNT, what does TXN-COUNT decide?

    Choose one answer.

    Show the answer: Question 13 of 17

    Answer: How many entries the table has at the moment, and so how long the record is

    The record was 16 bytes with a count of 2 and 21 with a count of 3.

  14. Question 14 of 17 How many bytes does the area of STATEMENT-REC take, room for every count from 1 to 10?

    Type a number.

    Show the answer: Question 14 of 17

    Answer: 56 bytes

    The area is sized for the maximum: 6 bytes of account and 10 x 5 bytes of amounts. A count of 11 described 61 bytes, 5 more than the area holds.

  15. Question 15 of 17 A program reads a record, moves its count field to TXN-COUNT and then moves the record to an output area. Where does the check of the count belong?

    Choose one answer.

    Show the answer: Question 15 of 17

    Answer: Right after the count is read, before any statement uses the record

    CHECK-COUNT in odo.cob shows the test: reject any count outside the limits of the OCCURS clause.

  16. Question 16 of 17 What did the default-dialect -debug build print for TXN-AMT(3) with TXN-COUNT = 2?

    Choose one answer.

    Show the answer: Question 16 of 17

    Answer: subscript out of bounds, with current maximum subscript 2

    The check is GnuCOBOL's ODO subscript check, which -debug turns on.

  17. Question 17 of 17 Why did the -debug build with -std=ibm let the write to TXN-AMT(3) happen?

    Choose one answer.

    Show the answer: Question 17 of 17

    Answer: Under that dialect GnuCOBOL 3.2 checks subscripts only against the maximum of the OCCURS, 10

    A program built for z/OS and tested with -std=ibm needs its own checks of the count to catch such a write.

Then build a lookup step where GnuCOBOL is installed:

A rate lookup that refuses an unsorted table

Load the eight slabs of search.cob from a file, check while loading that each code is greater than the one before it, and fail with a message when one is not. Then look up a list of codes with SEARCH ALL. Run it once with the slabs in order and once with 104 moved to the end, as in unsorted.cob, and compare the two runs.

Interview questions

Warm-up (fresher to 2–5 years): what must be true of a table before SEARCH ALL can be used on it?

Its OCCURS clause must declare ASCENDING KEY or DESCENDING KEY and INDEXED BY, and its entries must actually be in that order when the search runs. SEARCH ALL is a binary search: it looks at the middle of the range that can still hold the key and rules out the other half, so an entry out of order can sit in a half it has already ruled out. In the lesson, two slabs appended after 179 made SEARCH ALL report 104 and 162 as not found, without an error, while a code in the sorted part was still found. Sorting the table with SORT … ON ASCENDING KEY after loading it, or checking the order while loading, makes the search correct again. Unlike a serial SEARCH, SEARCH ALL sets its index itself.

Two harder questions follow: a rate lookup that started to miss codes, and a count that came from a damaged record. Decide your first step before you open each one:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    After a change that lets operators add new rate slabs, a nightly job charges the default rate to some accounts whose slab is in the rate table. The lookup uses SEARCH ALL, and the slabs are loaded from a file. What do you suspect, how do you confirm it, and how do you make the lookup safe?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    An input record carries a count of up to 10 amounts, which the program uses as the counter of an OCCURS DEPENDING ON table. One damaged record arrives with a count of 37, and the output for the records after it is garbled. Explain what happened and how you would harden the program.

    Model answer, Pro. First step and follow-ups

Organisations named on this page

IBM and the GnuCOBOL project are cited here as sources and have no connection with MySmartCoPilot. SEARCH, SORT and OCCURS DEPENDING ON are quoted from IBM’s documentation and GnuCOBOL’s NEWS, and the outputs and times are runs of GnuCOBOL 3.2.

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Where does a serial SEARCH start?

    At the current setting of the table's index. SET it to 1 just before every serial SEARCH: a search left where the previous one stopped reported a present code as not found.

  2. What does a table need before SEARCH ALL can be used on it?

    ASCENDING KEY or DESCENDING KEY and INDEXED BY on its OCCURS, and entries that really are in that order. SEARCH ALL sets its index itself.

  3. What happens when SEARCH ALL runs on a table that is out of key order?

    It can miss keys that are there, with no error: the binary search rules out the half where a misplaced key sits.

  4. How do you put a table into key order in place?

    Use the table form of SORT on the entries, with RT-CODE as the ascending key, once after loading. WITH DUPLICATES keeps entries with equal keys in their original order.

  5. At most how many probes does a binary search need for 5,000 entries?

    13, from the bound ⌊log₂ n⌋ + 1. A serial search tests every one of the 5,000 entries before it can report a miss.

  6. What does OCCURS 1 TO 10 TIMES DEPENDING ON TXN-COUNT do to a record's length?

    The record is as long as its count: 6 bytes of account plus 5 for each amount gave 16 bytes for 2 amounts and 21 for 3.

  7. What if the DEPENDING ON count lies outside the OCCURS range?

    IBM calls the behaviour undefined: a count of 11 gave 61 bytes in an area of 56, and -debug said nothing. Test any count that comes from input against the limits.

  8. Does a GnuCOBOL -debug build catch a subscript past the current DEPENDING ON count?

    In the default dialect, yes. Under -std=ibm it checks against the maximum only, so writing entry 3 while the count is 2 goes through.

Key takeaways

  • A serial SEARCH starts at the index’s current setting, so SET the index to 1 before each one; the search for 117 that skipped this step reported a code that was there as not found.
  • SEARCH ALL is a binary search that is correct only on a table in its declared key order; two misplaced slabs made it miss 104 and 162 without an error.
  • The table SORT puts the entries in key order in place, after which SEARCH ALL found every code.
  • A miss costs a serial search every entry and a binary search at most ⌊log₂ n⌋ + 1 probes, and the measured serial times grew with the table while SEARCH ALL’s hardly changed.
  • OCCURS DEPENDING ON makes the record as long as its count, 16 bytes for 2 amounts and 21 for 3, and a count outside the OCCURS range is undefined, so it is validated; GnuCOBOL’s IBM dialect checks subscripts only against the maximum.

Terms in this lesson

ASCENDING KEY
The phrase of an OCCURS clause that names the field a table is kept in order by, smallest first. SEARCH ALL relies on that order, and the table SORT can produce it.
binary search
A search that compares the key with the middle entry of the range that can still hold it and keeps one half, so n entries need at most the whole part of log2(n) plus 1 probes. It needs the entries in key order.
variable-length table
A table declared with OCCURS min TO max TIMES DEPENDING ON a counter. The counter's current value is the number of entries, so it sets the length of the table and of its record.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.