Conditions, IF and EVALUATE

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against ISO/IEC 1989:2023 (edition 3, January 2023; an edition 4 is upcoming, in DIS ballot), GnuCOBOL 3.2 (28 July 2023, the latest release) and IBM Enterprise COBOL for z/OS 6.5 (PTFs through September 2026), and was checked on 2026-10-20. Every portable example below was compiled with cobc -x -Wall (GnuCOBOL 3.2.0, fixed reference format) without a single warning and run; the output and the diagnostics shown are real, and the few examples that need another option or that are deliberately wrong say so. The ISO standard is paywalled and was not read: the rules come from the Enterprise COBOL Language Reference and Programming Guide and from the GnuCOBOL Programmer’s Guide, and where the two differ the page says so.

A COBOL program makes decisions with a conditional expression: an expression whose value is true or false. The Language Reference lists the statements that take one: IF, EVALUATE, PERFORM (the UNTIL phrase) and SEARCH (the WHEN phrase). This page covers how conditions are written and combined, then the two statements that branch on them, IF and EVALUATE. The data items the examples test come from the Circulation library back office: the 88-level names BOOK-ACTIVE, BOOK-WITHDRAWN, MEMBER-ACTIVE, MEMBER-SUSPENDED, LOAN-CHECKOUT and LOAN-RETURN are defined in the copybooks BOOKREC, MEMBREC and LOANREC, and how a level-88 entry is declared and set is in VALUE, INITIALIZE and Condition Names. Loops that end on a condition are in PERFORM and Structured Programming, and the table search that uses the same conditions is in Searching and Sorting Tables.

The six simple conditions

A condition is simple or complex. Enterprise COBOL 6.5 defines six simple conditions. Any number of paired parentheses may enclose a condition without changing whether it is simple or complex.

Simple condition What it tests Example

Relation

Compares two operands: data items, literals, functions, arithmetic expressions, index-names, pointers

WS-COPIES > 2, BOOK-STATUS = "W"

Class

Whether a data item holds only digits, only letters, only lowercase or uppercase letters, or only the characters of a user-defined class

BOOK-ISBN IS NUMERIC, BOOK-ISBN IS ISBN-CHAR

Sign

Whether a numeric operand is positive, negative or zero

MEMBER-FINES IS POSITIVE

Condition-name

Whether a conditional variable has one of the values of a level-88 entry

MEMBER-SUSPENDED

Switch-status

Whether an external switch is on or off

DRY-RUN

Omitted-argument

Whether the caller left out an optional argument

LK-DAYS IS OMITTED

Each has a truth value, and the logical operators NOT, AND and OR combine them into complex conditions. The example below uses every kind once, in the form that is accepted by both compilers (the UPSI-0 switch name needs -std=ibm; see the section on switch-status conditions).

Example: every kind of simple condition

       IDENTIFICATION DIVISION.
       PROGRAM-ID. CONDKINDS.
       ENVIRONMENT DIVISION.
       CONFIGURATION SECTION.
       SPECIAL-NAMES.
           CLASS ISBN-CHAR IS "0" THRU "9" "X"
           UPSI-0 IS DRY-RUN-SWITCH
               ON STATUS IS DRY-RUN
               OFF STATUS IS LIVE-RUN.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
           COPY BOOKREC.
           COPY MEMBREC.
       01  WS-CODE-SHORT           PIC X(3)    VALUE "ABC".
       01  WS-CODE-LONG            PIC X(5)    VALUE "ABC  ".
       01  WS-ZIP-TEXT             PIC X(5)    VALUE "00042".
       01  WS-ZIP-NUM              PIC 9(5)    VALUE 42.
       01  WS-AMOUNT-1             PIC S9(5)V99 VALUE 12.5.
       01  WS-AMOUNT-2             PIC 9(3)V9  VALUE 12.5.
       01  WS-ADJUST               PIC S9(3)   VALUE -7.
       01  WS-FLAG                 PIC X       VALUE "N".
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE "9780000000011" TO BOOK-ISBN
           MOVE "A" TO BOOK-STATUS
           MOVE "S" TO MEMBER-STATUS
           MOVE 12.50 TO MEMBER-FINES
      * relation conditions
           IF WS-CODE-SHORT = WS-CODE-LONG
               DISPLAY "SHORT PADDED WITH SPACES: EQUAL"
           END-IF
           IF WS-ZIP-TEXT = WS-ZIP-NUM
               DISPLAY "ALPHANUMERIC 00042 = NUMERIC 42: EQUAL"
           END-IF
           IF WS-AMOUNT-1 = WS-AMOUNT-2
               DISPLAY "12.50 = 012.5: ALGEBRAIC COMPARISON"
           END-IF
           IF WS-CODE-SHORT GREATER THAN OR EQUAL TO "ABB"
               DISPLAY "ABC >= ABB"
           END-IF
           IF WS-CODE-SHORT IS NOT LESS THAN "ABD"
               DISPLAY "NOT REACHED"
           ELSE
               DISPLAY "ABC IS NOT LESS THAN ABD IS FALSE"
           END-IF
      * class conditions
           IF BOOK-ISBN IS NUMERIC
               DISPLAY "ISBN IS NUMERIC"
           END-IF
           IF BOOK-ISBN IS ISBN-CHAR
               DISPLAY "ISBN USES ONLY ISBN-CHAR CHARACTERS"
           END-IF
           MOVE "97800000X0011" TO BOOK-ISBN
           IF BOOK-ISBN IS NOT NUMERIC
               DISPLAY "ISBN WITH X IS NOT NUMERIC"
           END-IF
           MOVE "COBOL BASICS" TO BOOK-TITLE
           IF BOOK-TITLE IS ALPHABETIC
               DISPLAY "COBOL BASICS IS ALPHABETIC"
           END-IF
           IF BOOK-TITLE IS ALPHABETIC-UPPER
               DISPLAY "COBOL BASICS IS ALPHABETIC-UPPER"
           END-IF
      * sign conditions
           IF MEMBER-FINES IS POSITIVE
               DISPLAY "FINES POSITIVE"
           END-IF
           IF WS-ADJUST IS NEGATIVE
               DISPLAY "ADJUSTMENT NEGATIVE"
           END-IF
           IF WS-ADJUST + 7 IS ZERO
               DISPLAY "ADJUSTMENT + 7 IS ZERO"
           END-IF
      * condition-name conditions
           IF MEMBER-SUSPENDED
               DISPLAY "MEMBER SUSPENDED"
           END-IF
           IF NOT BOOK-WITHDRAWN
               DISPLAY "BOOK NOT WITHDRAWN"
           END-IF
      * switch-status conditions
           IF DRY-RUN
               DISPLAY "DRY RUN"
           END-IF
           IF LIVE-RUN
               DISPLAY "LIVE RUN"
           END-IF
      * combined conditions
           IF MEMBER-SUSPENDED OR MEMBER-FINES > 10
               AND NOT BOOK-WITHDRAWN
               DISPLAY "COMBINED CONDITION TRUE"
           END-IF
           STOP RUN.

The program was compiled and run with cobc -x -Wall -std=ibm kinds.cob:

SHORT PADDED WITH SPACES: EQUAL
ALPHANUMERIC 00042 = NUMERIC 42: EQUAL
12.50 = 012.5: ALGEBRAIC COMPARISON
ABC >= ABB
ABC IS NOT LESS THAN ABD IS FALSE
ISBN IS NUMERIC
ISBN USES ONLY ISBN-CHAR CHARACTERS
ISBN WITH X IS NOT NUMERIC
COBOL BASICS IS ALPHABETIC
COBOL BASICS IS ALPHABETIC-UPPER
FINES POSITIVE
ADJUSTMENT NEGATIVE
ADJUSTMENT + 7 IS ZERO
MEMBER SUSPENDED
BOOK NOT WITHDRAWN
LIVE RUN
COMBINED CONDITION TRUE

The first five lines show the comparison rules described next; the class, sign, condition-name and switch-status lines show the other kinds; the last line is a complex condition. UPSI-0 was off, so LIVE-RUN was true.

Relation conditions

A relation condition compares two operands with a relational operator. The word forms and the symbols are interchangeable; NOT in front of the operator reverses it. The two characters of >= and <= must not be separated by a space, and every operator must be preceded and followed by a space.

Words Symbol Meaning

IS GREATER THAN

IS >

greater than

IS LESS THAN

IS <

less than

IS EQUAL TO

IS =

equal to

IS GREATER THAN OR EQUAL TO

IS >=

greater than or equal to

IS LESS THAN OR EQUAL TO

IS <=

less than or equal to

IS NOT GREATER THAN, IS NOT LESS THAN, IS NOT EQUAL TO

IS NOT >, IS NOT <, IS NOT =

the opposite of each

The comparison that is performed depends on the classes of the two operands (Enterprise COBOL Language Reference, Tables 23 and 24; the operand pairs that are not allowed are the blank cells in those tables). The rules that matter every day:

  • Numeric against numeric compares algebraic values. The number of digits, the USAGE, the position of the decimal point and the sign representation are not significant; unsigned operands are positive, and all zero values are equal. 12.50 in a PIC S9(5)V99 item equals 12.5 in a PIC 9(3)V9 item, as in the example above.

  • Alphanumeric against alphanumeric compares character by character from the left, using the collating sequence in use (the native one, or the one selected with PROGRAM COLLATING SEQUENCE). If the operands differ in length, the shorter is treated as if it were extended to the right with spaces, so "ABC" equals "ABC ".

  • A numeric integer against an alphanumeric item is allowed only for integers: the integer is treated as if it had been moved to an alphanumeric item with as many characters as the integer has digits. 00042 in a PIC X(5) item equals the PIC 9(5) value 42. A numeric item with decimals against an alphanumeric item is not allowed by Enterprise COBOL (the table marks only integer items as permitted). GnuCOBOL 3.2.0 accepts IF BOOK-TITLE = 12.5 in every dialect without a diagnostic and the test is simply false, so the compiler will not catch it for you.

  • Group items are compared as alphanumeric strings of bytes, without any conversion. Comparing a group that contains a packed or binary field with a numeric literal is therefore a comparison of bytes, which gives a result that is predictable only when both sides have the same data representation.

  • Figurative constants take the class of the other operand: ZERO against a numeric item is a numeric comparison, against an alphanumeric item it is the character 0; SPACE and HIGH-VALUE are characters (see Words, Literals and Figurative Constants).

  • National and UTF-8 operands are compared in their own encodings, and a PROGRAM COLLATING SEQUENCE has no effect on UTF-8 comparisons. National Data, UTF-8 and Code Pages covers them.

  • Pointers, index-names and object references have their own relation conditions (only equal and not equal for pointers; an index-name is compared by its occurrence number). See Pointers and Dynamic Storage and Tables and OCCURS.

A relation condition must contain at least one reference to an identifier, which is why IF "A" = "B" is not a meaningful test; the next sections show what GnuCOBOL says about it.

Class conditions

A class condition tests the content of a data item. The Language Reference gives the forms that are valid for each kind of item (Table 21):

Item referenced Valid tests (each also with NOT)

Alphabetic (PIC A)

ALPHABETIC, ALPHABETIC-LOWER, ALPHABETIC-UPPER, a class-name

Alphanumeric, alphanumeric-edited, numeric-edited

NUMERIC, ALPHABETIC, ALPHABETIC-LOWER, ALPHABETIC-UPPER, a class-name

Zoned decimal (USAGE DISPLAY) or packed decimal (COMP-3)

NUMERIC

National

NUMERIC, ALPHABETIC, ALPHABETIC-LOWER, ALPHABETIC-UPPER

DBCS

DBCS, KANJI

What each test means:

  • NUMERIC: only the digits 0 to 9, with or without an operational sign. If the PICTURE has no sign, a sign in the data makes the item not numeric. If it has a sign, the item must be elementary and carry a valid sign. Which sign codes are valid depends on the NUMPROC compiler option and the NUMCLS installation option, and invalid zone codes are treated as nonnumeric by the test even when INVDATA tolerates them in comparisons. A group item can take NUMERIC only if it has no signed elementary item.

  • ALPHABETIC is the letters A to Z, a to z and the space; ALPHABETIC-LOWER and ALPHABETIC-UPPER are the lowercase and uppercase letters and the space. An all-spaces item therefore passes all three, and a numeric-looking PIC X item passes none of them.

  • A class-name is a user-defined class declared in SPECIAL-NAMES: the item passes if it contains only the listed characters.

  • DBCS and KANJI apply to DISPLAY-1 items and range-check the byte pairs.

The example above declares CLASS ISBN-CHAR IS "0" THRU "9" "X", the character set of an ISBN-10 or ISBN-13 (the X check digit), and tests BOOK-ISBN IS ISBN-CHAR. A value with an X in the middle still passes ISBN-CHAR but fails NUMERIC, as the output shows.

GnuCOBOL is more permissive than the table above: its Programmer’s Guide says it has no restriction on group items or PIC A items with NUMERIC, nor on PIC 9 items with ALPHABETIC, and 3.2.0 compiled an ALPHABETIC test on PIC 9 and COMP-3 items without any diagnostic. Code that is meant to run on z/OS should stay inside Table 21.

Validating input with class tests

The numeric class test is the standard guard against invalid data. The compiler assumes that the contents of a data item agree with its PICTURE and USAGE; when they do not (a record from another system, a field overwritten through a REDEFINES), references to the item are undefined. Test before using. The program checks five raw transaction records before it trusts LOANREC:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. LOANVALID.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
           COPY LOANREC.
       01  WS-RAW-VALUES.
           05  FILLER  PIC X(21)   VALUE "O00010000000120261020".
           05  FILLER  PIC X(21)   VALUE "O00A10000000120261020".
           05  FILLER  PIC X(21)   VALUE "R      00000120261020".
           05  FILLER  PIC X(21)   VALUE "X00010000000120261020".
           05  FILLER  PIC X(21)   VALUE "O0001000000012026102 ".
       01  WS-RAW-TABLE REDEFINES WS-RAW-VALUES.
           05  WS-RAW              PIC X(21)   OCCURS 5 TIMES.
       01  WS-SUB                  PIC 9       VALUE ZERO.
       01  WS-MEMBER-TEXT          PIC X(6)    VALUE SPACES.
       PROCEDURE DIVISION.
       MAIN-PARA.
           PERFORM VARYING WS-SUB FROM 1 BY 1 UNTIL WS-SUB > 5
               MOVE WS-RAW (WS-SUB) TO LOAN-RECORD
               IF (LOAN-CHECKOUT OR LOAN-RETURN)
                   AND LOAN-MEMBER-ID IS NUMERIC
                   AND LOAN-BOOK-ID IS NUMERIC
                   AND LOAN-DATE IS NUMERIC
                   DISPLAY WS-SUB ": VALID " WS-RAW (WS-SUB)
               ELSE
                   DISPLAY WS-SUB ": REJECT " WS-RAW (WS-SUB)
               END-IF
           END-PERFORM
           IF WS-MEMBER-TEXT IS NUMERIC
               DISPLAY "SPACES ARE NUMERIC"
           ELSE
               DISPLAY "SPACES ARE NOT NUMERIC"
           END-IF
           IF WS-MEMBER-TEXT IS ALPHABETIC
               DISPLAY "SPACES ARE ALPHABETIC"
           END-IF
           STOP RUN.
1: VALID O00010000000120261020
2: REJECT O00A10000000120261020
3: REJECT R      00000120261020
4: REJECT X00010000000120261020
5: REJECT O0001000000012026102
SPACES ARE NOT NUMERIC
SPACES ARE ALPHABETIC

The parentheses around LOAN-CHECKOUT OR LOAN-RETURN matter, and the next sections explain why. Record 3 shows that a blank field is not numeric (but is alphabetic, as the last two lines show for a PIC X item). On z/OS the NUMCHECK(ZON,PAC) compiler option can have the compiler generate implicit numeric class tests for zoned and packed decimal items that are sending items, which validates sign fields against the NUMPROC option too. Where the data comes from and how a MOVE treats it is in MOVE and Data Movement.

Sign conditions

A sign condition tests whether a numeric operand is POSITIVE (greater than zero), NEGATIVE (less than zero) or ZERO; NOT reverses it, and NOT ZERO is true for a positive or a negative value. The operand can be a numeric identifier or an arithmetic expression that contains at least one variable, so IF WS-ADJUST + 7 IS ZERO is valid, as in the example. An unsigned operand is either positive or zero. On z/OS the result depends on the NUMPROC option.

Condition-name conditions

A condition-name is the name of a level-88 entry. The condition MEMBER-SUSPENDED is true when the conditional variable MEMBER-STATUS has any of the values (or lies in any of the THRU ranges) of the entry. The rules of comparison are those of relation conditions, and it is the same as writing the relation by hand: IF MEMBER-SUSPENDED replaces IF MEMBER-STATUS = "S". Condition-names are allowed for alphanumeric, DBCS, national, UTF-8 and floating-point items as well as numeric ones. Prefer the name to the literal: the meaning of the value is written once, in the data division, and SET MEMBER-SUSPENDED TO TRUE changes the state without repeating the code. That SET statement, and the WHEN SET TO FALSE phrase, are in the page on VALUE, INITIALIZE and Condition Names.

Switch-status conditions

A switch-status condition tests an external on/off switch. The programmer associates a condition-name with the on or off value of a switch in SPECIAL-NAMES, then tests the name:

  • In Enterprise COBOL the switch is one of the eight UPSI (user-programmable status indicator) switches UPSI-0 to UPSI-7; the name is associated with the value 1 (on) or 0 (off). The value is set outside the program, for example to select a year-end run.

  • In GnuCOBOL the switch is SWITCH-0 to SWITCH-36, and it is set from the environment with COB_SWITCH_n=ON before the run (COB_SWITCH_1 for SWITCH-1).

  • GnuCOBOL 3.2.0 accepts UPSI-0 as a synonym of SWITCH-0 only with -std=ibm. Compiled without it, the example gives the error below, and with -std=ibm the name SWITCH-1 is rejected in the same way. Setting COB_SWITCH_0=ON for the -std=ibm build of the example made DRY-RUN true, and COB_SWITCH_1=ON left it false.

kinds.cob:7: error: invalid system-name 'UPSI-0'
    5 |        SPECIAL-NAMES.
    6 |            CLASS ISBN-CHAR IS "0" THRU "9" "X"
    7 >            UPSI-0 IS DRY-RUN-SWITCH

This output is from compiling the example above without -std=ibm. Switch conditions are a way to vary a run without recompiling, such as a dry-run mode for the nightly run; for anything more than a few modes, read the setting from a parameter or a file instead.

The omitted-argument condition

A called program can make a parameter optional and test whether the caller passed it. Enterprise COBOL 6.5 documents the OPTIONAL phrase of PROCEDURE DIVISION USING (it is not allowed when the LP(64) compiler option is in effect) and the condition data-name IS [NOT] OMITTED. The condition is true when the caller passed OMITTED for that argument, when the argument was a trailing one that the call left out, or when the argument is itself an omitted formal parameter of the caller. The data item must be defined in the program that tests it and be named in an OPTIONAL phrase. The GnuCOBOL 3.2 Programmer’s Guide lists OMITTED among its class conditions. The example is a program pair that calls LOANDAYS once with a loan period and once without:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. OMITMAIN.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-LIMIT                PIC 9(3)    VALUE 21.
       PROCEDURE DIVISION.
       MAIN-PARA.
           CALL "LOANDAYS" USING WS-LIMIT
           CALL "LOANDAYS" USING OMITTED
           STOP RUN.
       END PROGRAM OMITMAIN.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. LOANDAYS.
       DATA DIVISION.
       LINKAGE SECTION.
       01  LK-DAYS                 PIC 9(3).
       PROCEDURE DIVISION USING OPTIONAL LK-DAYS.
       MAIN-PARA.
           IF LK-DAYS IS OMITTED
               DISPLAY "NO LIMIT PASSED: DEFAULT 14 DAYS"
           ELSE
               DISPLAY "LIMIT PASSED: " LK-DAYS " DAYS"
           END-IF
           GOBACK.
       END PROGRAM LOANDAYS.

Compiled with cobc -x -Wall omit.cob:

LIMIT PASSED: 021 DAYS
NO LIMIT PASSED: DEFAULT 14 DAYS

With -std=ibm the same file compiles, runs identically, and gives warning: USING OPTIONAL used [-Wdialect], so a build that must be free of warnings in that dialect cannot use it. The way arguments are passed, BY REFERENCE, BY CONTENT and BY VALUE, and what OMITTED means in the CALL statement, are in Subprograms, CALL and Linkage.

Complex conditions: NOT, AND and OR

Simple conditions are combined with the logical operators, each of which must be preceded and followed by a space.

Operator Name True when

NOT

logical negation

the condition after it is false

AND

logical conjunction

both conditions are true

OR

logical inclusive or

either or both conditions are true

Precedence from highest to lowest: arithmetic operations, the simple conditions themselves, NOT, AND, OR. Parentheses override it. The Language Reference gives this example, which is evaluated as the parenthesized form below it:

       NOT A IS GREATER THAN B OR A + B IS EQUAL TO C AND D IS POSITIVE
       (NOT (A IS GREATER THAN B)) OR
       (((A + B) IS EQUAL TO C) AND (D IS POSITIVE))

Order of evaluation. Conditions within one level are evaluated from left to right, and evaluation of that level stops as soon as its truth value is known; arithmetic expressions and functions inside a condition are evaluated only if and when that condition is reached. This is what makes a guard such as "is the subscript valid, and only then the table entry" safe. GnuCOBOL 3.2.0 behaves the same way. In the program below the first test is guarded and the second is not; with -debug the second one is caught by the subscript check:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. SHORTCIRC.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-I                    PIC 9       VALUE ZERO.
       01  WS-TABLE.
           05  WS-ENTRY            PIC X       OCCURS 3 TIMES VALUE "A".
       PROCEDURE DIVISION.
       MAIN-PARA.
           IF WS-I > 0 AND WS-ENTRY (WS-I) = "A"
               DISPLAY "FOUND"
           ELSE
               DISPLAY "GUARDED"
           END-IF
           IF WS-ENTRY (WS-I) = "A" AND WS-I > 0
               DISPLAY "FOUND"
           ELSE
               DISPLAY "NOT GUARDED"
           END-IF
           STOP RUN.

Compiled with cobc -x -Wall -debug shortcirc.cob; the first line is the output of the guarded test and the rest is the runtime error of the second one (the program stops there and returns exit status 1):

GUARDED
libcob: shortcirc.cob:15: error: subscript of 'WS-ENTRY' out of bounds: 0

 Last statement of "SHORTCIRC" was IF
	MAIN-PARA at shortcirc.cob:15
	ENTRY SHORTCIRC at shortcirc.cob:9
 Started by ./shortcirc.out

Without -debug GnuCOBOL does not check the subscript: the run without it printed NOT GUARDED, having read the storage in front of the table. Table subscript checking options are covered in Tables and OCCURS.

Two rules help a reader: write the cheap or protective test first, and put parentheses around any mix of AND and OR even where the precedence rules would give the same answer. Parentheses are never needed when a condition uses only AND or only OR. The Language Reference also notes that NOT A IS EQUAL TO B and NOT (A IS EQUAL TO B) are the same.

Abbreviated combined relation conditions

When relation conditions are written one after another, any condition after the first can omit its subject, or its subject and its relational operator. The omitted parts are taken from the last stated subject and the last stated operator:

Abbreviated Unabbreviated equivalent

A = B AND NOT < C OR D

A = B) AND (A NOT < C OR (A NOT < D)

A NOT > B OR C

(A NOT > B) OR (A NOT > C)

NOT A = B OR C

(NOT (A = B)) OR (A = C)

NOT (A = B OR < C)

NOT A = B) OR (A < C

(The first four rows of Table 31 in the Language Reference.) The rules, in the order in which they cause trouble:

  • NOT is part of the relational operator when it is immediately followed by GREATER THAN, >, LESS THAN, <, EQUAL TO or =. In any other position it is a logical NOT that negates only the single relation condition that follows it: the negation is not carried to the objects that are abbreviated after it.

  • A parenthesis right after a relational operator distributes that operator over the objects inside it, as in WS-COPIES = (3 OR 4 OR 5). A distributed operator may not contain a simple condition, another relational operator, or a NOT immediately after the left parenthesis.

  • The insertion of an omitted subject or operator stops when another simple condition (including a condition-name) is found, or at the right parenthesis that closes a parenthesis opened to the left of the subject.

  • Two consecutive logical NOT`s cancel each other and produce an S-level message; `A = B AND NOT NOT = C is legal, because the second NOT belongs to the operator.

Example: the abbreviated forms, and the classic trap

The program prints the truth value of five conditions for a number of copies from 0 to 11. Column A is > 2 AND < 10 with the subject omitted. Columns B and C are the trap: NOT = 1 OR 2 means "not 1, or not 2" and is true for every number, while the condition that was probably meant is NOT = 1 AND 2. Column D omits the operator as well, and column E distributes = over a parenthesized list. The last four lines show NOT and the precedence of AND:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. ABBREV.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-COPIES               PIC 9(3).
       01  WS-RESULTS.
           05  WS-R1               PIC X(5).
           05  WS-R2               PIC X(5).
           05  WS-R3               PIC X(5).
           05  WS-R4               PIC X(5).
           05  WS-R5               PIC X(5).
       01  WS-MEMBER-STATUS        PIC X.
       01  WS-A                    PIC 9       VALUE 1.
       01  WS-B                    PIC 9       VALUE 0.
       01  WS-C                    PIC 9       VALUE 0.
       PROCEDURE DIVISION.
       MAIN-PARA.
           DISPLAY "COPIES  A      B      C      D      E"
           PERFORM VARYING WS-COPIES FROM 0 BY 1 UNTIL WS-COPIES > 11
      * A: abbreviated subject, same as (COPIES > 2) AND (COPIES < 10)
               IF WS-COPIES > 2 AND < 10
                   MOVE "TRUE " TO WS-R1
               ELSE
                   MOVE "FALSE" TO WS-R1
               END-IF
      * B: the classic trap, (COPIES NOT = 1) OR (COPIES NOT = 2)
               IF WS-COPIES NOT = 1 OR 2
                   MOVE "TRUE " TO WS-R2
               ELSE
                   MOVE "FALSE" TO WS-R2
               END-IF
      * C: what B was meant to say
               IF WS-COPIES NOT = 1 AND 2
                   MOVE "TRUE " TO WS-R3
               ELSE
                   MOVE "FALSE" TO WS-R3
               END-IF
      * D: operator abbreviated too, (COPIES < 3) OR (COPIES > 9)
               IF WS-COPIES < 3 OR > 9
                   MOVE "TRUE " TO WS-R4
               ELSE
                   MOVE "FALSE" TO WS-R4
               END-IF
      * E: parentheses distribute the operator over the objects
               IF WS-COPIES = (3 OR 4 OR 5)
                   MOVE "TRUE " TO WS-R5
               ELSE
                   MOVE "FALSE" TO WS-R5
               END-IF
               DISPLAY WS-COPIES "    " WS-R1 "  " WS-R2 "  " WS-R3
                   "  " WS-R4 "  " WS-R5
           END-PERFORM
           MOVE "B" TO WS-MEMBER-STATUS
      * NOT before an unrelated relation negates only that relation:
      * (NOT (STATUS = "A")) OR (STATUS = "B")
           IF NOT WS-MEMBER-STATUS = "A" OR "B"
               DISPLAY "NOT STATUS = A OR B: TRUE FOR B"
           ELSE
               DISPLAY "NOT STATUS = A OR B: FALSE FOR B"
           END-IF
      * parentheses negate the whole alternative
           IF NOT (WS-MEMBER-STATUS = "A" OR "B")
               DISPLAY "NOT (STATUS = A OR B): TRUE FOR B"
           ELSE
               DISPLAY "NOT (STATUS = A OR B): FALSE FOR B"
           END-IF
      * AND binds tighter than OR: A = 1 OR (B = 1 AND C = 1)
           IF WS-A = 1 OR WS-B = 1 AND WS-C = 1
               DISPLAY "A OR B AND C: TRUE"
           ELSE
               DISPLAY "A OR B AND C: FALSE"
           END-IF
           IF (WS-A = 1 OR WS-B = 1) AND WS-C = 1
               DISPLAY "(A OR B) AND C: TRUE"
           ELSE
               DISPLAY "(A OR B) AND C: FALSE"
           END-IF
           STOP RUN.
COPIES  A      B      C      D      E
000    FALSE  TRUE   TRUE   TRUE   FALSE
001    FALSE  TRUE   FALSE  TRUE   FALSE
002    FALSE  TRUE   FALSE  TRUE   FALSE
003    TRUE   TRUE   TRUE   FALSE  TRUE
004    TRUE   TRUE   TRUE   FALSE  TRUE
005    TRUE   TRUE   TRUE   FALSE  TRUE
006    TRUE   TRUE   TRUE   FALSE  FALSE
007    TRUE   TRUE   TRUE   FALSE  FALSE
008    TRUE   TRUE   TRUE   FALSE  FALSE
009    TRUE   TRUE   TRUE   FALSE  FALSE
010    FALSE  TRUE   TRUE   TRUE   FALSE
011    FALSE  TRUE   TRUE   TRUE   FALSE
NOT STATUS = A OR B: TRUE FOR B
NOT (STATUS = A OR B): FALSE FOR B
A OR B AND C: TRUE
(A OR B) AND C: FALSE

Column B is true on every row: that is the consequence of the rule, and the compiler does not warn about it (the program compiles clean with -Wall, and also with -std=ibm). The same program shows that NOT STATUS = "A" OR "B" is true for a status of B, whereas NOT (STATUS = "A" OR "B") is false for it. The GnuCOBOL Programmer’s Guide, in its section on combined conditions, shows only the plain abbreviation ACCOUNT-STATUS = 1 OR 2 OR 7; the behaviour of 3.2.0 on the forms tried above agrees with the IBM description.

Abbreviated conditions are compact, and the compact form is the one people misread. Use them for the plain forms (a list of values, a range written X > 2 AND < 10), and for anything involving NOT spell the conditions out or use a level-88 name that lists the values or a THRU range.

The IF statement

IF evaluates a condition and runs one of two alternative statements. In the form to use today:

       IF condition [THEN]
           statement-1 ...
       [ELSE
           statement-2 ...]
       END-IF
  • THEN is optional and has no effect on the program.

  • statement-1 runs when the condition is true and statement-2 when it is false. Each is a series of imperative statements, and may include another conditional statement, in which case the IF is nested.

  • END-IF is the explicit scope terminator. Without it, the scope of the IF ends at the next separator period (and an ELSE belongs to the nearest preceding IF that has not been terminated).

  • NEXT SENTENCE, or CONTINUE, can stand where a statement is required. See the section below.

Nested IF statements are matched left to right: an ELSE goes with the nearest preceding IF that has no ELSE and has not been terminated, and an END-IF goes with the nearest preceding IF that has not been terminated. With END-IF on every IF the matching is explicit and the indentation tells the truth. If the ELSE is omitted, every statement between the condition and the END-IF (or the period) belongs to the true branch.

Explicit scope terminators and the period

Every conditional statement (IF, EVALUATE, READ, SEARCH, an ADD with an ON SIZE ERROR phrase, and so on) has an explicit scope terminator (END-IF, END-EVALUATE, END-READ, END-SEARCH, END-ADD) that turns it into an imperative statement. A sentence is one or more statements ended by a period. The period is the implicit scope terminator: it ends every statement that is still open, however deeply nested, and that is the cause of the best-known COBOL bug.

The stray period

A period inside a block of statements ends the whole sentence, including the IF that contains it. Everything after it runs unconditionally. The next program counts three members, one of them suspended. BUGGY-CHECK is deliberately wrong: the author wanted to count a suspended member as rejected and post the others, and put a period after the ADD that counts the rejection. The ADD 1 TO WS-POSTED that follows then belongs to no IF at all. FIXED-CHECK states the intent with ELSE and END-IF:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. STRAYDOT.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
           COPY MEMBREC.
       01  WS-STATUS-VALUES        PIC X(3)    VALUE "ASA".
       01  WS-STATUS-TABLE REDEFINES WS-STATUS-VALUES.
           05  WS-STATUS           PIC X       OCCURS 3 TIMES.
       01  WS-SUB                  PIC 9       VALUE ZERO.
       01  WS-POSTED               PIC 9       VALUE ZERO.
       01  WS-REJECTED             PIC 9       VALUE ZERO.
       PROCEDURE DIVISION.
       MAIN-PARA.
           DISPLAY "STRAY PERIOD"
           MOVE ZERO TO WS-POSTED WS-REJECTED
           PERFORM VARYING WS-SUB FROM 1 BY 1 UNTIL WS-SUB > 3
               MOVE WS-STATUS (WS-SUB) TO MEMBER-STATUS
               PERFORM BUGGY-CHECK
           END-PERFORM
           DISPLAY "  POSTED " WS-POSTED " REJECTED " WS-REJECTED
           DISPLAY "SCOPE TERMINATOR"
           MOVE ZERO TO WS-POSTED WS-REJECTED
           PERFORM VARYING WS-SUB FROM 1 BY 1 UNTIL WS-SUB > 3
               MOVE WS-STATUS (WS-SUB) TO MEMBER-STATUS
               PERFORM FIXED-CHECK
           END-PERFORM
           DISPLAY "  POSTED " WS-POSTED " REJECTED " WS-REJECTED
           STOP RUN.

       BUGGY-CHECK.
           IF MEMBER-SUSPENDED
               ADD 1 TO WS-REJECTED.
           ADD 1 TO WS-POSTED.

       FIXED-CHECK.
           IF MEMBER-SUSPENDED
               ADD 1 TO WS-REJECTED
           ELSE
               ADD 1 TO WS-POSTED
           END-IF.
STRAY PERIOD
  POSTED 3 REJECTED 1
SCOPE TERMINATOR
  POSTED 2 REJECTED 1

All three members were posted by the buggy version, including the suspended one, and the compiler said nothing. The bug is silent because a period after a statement is legal almost everywhere. The signs to look for: a period in the middle of a paragraph, a period at the end of a line that continues an IF visually, and a statement that "always runs" even though it is indented under an IF. The cure is a habit rather than a tool:

  • give every IF, EVALUATE, READ, SEARCH and PERFORM its scope terminator, and write a single period, on a line of its own or at the end of the last statement, to end each paragraph;

  • never put a period inside a paragraph other than at its end;

  • indent by the structure, not by the period.

When the period comes before an ELSE, the compiler can see the problem, because an ELSE with no open IF is a syntax error. This deliberately wrong program puts a period after the first branch:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. BADELSE.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
           COPY MEMBREC.
       01  WS-REJECTED             PIC 9       VALUE ZERO.
       01  WS-POSTED               PIC 9       VALUE ZERO.
       PROCEDURE DIVISION.
       MAIN-PARA.
           IF MEMBER-SUSPENDED
               ADD 1 TO WS-REJECTED.
           ELSE
               ADD 1 TO WS-POSTED
           END-IF.
           STOP RUN.

Real diagnostics from cobc -x -Wall badelse.cob:

badelse.cob: in paragraph 'MAIN-PARA':
badelse.cob:12: error: syntax error, unexpected ELSE
   10 |            IF MEMBER-SUSPENDED
   11 |                ADD 1 TO WS-REJECTED.
   12 >            ELSE
   13 |                ADD 1 TO WS-POSTED
   14 |            END-IF.
badelse.cob:14: error: syntax error, unexpected END-IF
   12 |            ELSE
   13 |                ADD 1 TO WS-POSTED
   14 >            END-IF.
   15 |            STOP RUN.
   16 | <EOF>

The second error is a consequence of the first: the END-IF has no IF to close.

NEXT SENTENCE versus CONTINUE

CONTINUE is a no-operation statement: it says that no executable instruction is present, and control passes to the next statement. NEXT SENTENCE is different, and it is an old construct you may meet in programs written before the 1985 standard: control passes to an implicit CONTINUE immediately after the next separator period. In an IF that is terminated by a period the two behave the same, as the program below shows (the NEXT SENTENCE paragraph is old code you may meet, kept here to read it with the modern form next to it):

       IDENTIFICATION DIVISION.
       PROGRAM-ID. NEXTSENT.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
           COPY MEMBREC.
       01  WS-STATUS-VALUES        PIC X(3)    VALUE "ASA".
       01  WS-STATUS-TABLE REDEFINES WS-STATUS-VALUES.
           05  WS-STATUS           PIC X       OCCURS 3 TIMES.
       01  WS-SUB                  PIC 9       VALUE ZERO.
       PROCEDURE DIVISION.
       MAIN-PARA.
           DISPLAY "NEXT SENTENCE"
           PERFORM VARYING WS-SUB FROM 1 BY 1 UNTIL WS-SUB > 3
               MOVE WS-STATUS (WS-SUB) TO MEMBER-STATUS
               PERFORM NEXT-SENTENCE-CHECK
           END-PERFORM
           DISPLAY "CONTINUE"
           PERFORM VARYING WS-SUB FROM 1 BY 1 UNTIL WS-SUB > 3
               MOVE WS-STATUS (WS-SUB) TO MEMBER-STATUS
               PERFORM CONTINUE-CHECK
           END-PERFORM
           STOP RUN.

       NEXT-SENTENCE-CHECK.
           IF MEMBER-SUSPENDED
               NEXT SENTENCE
           ELSE
               DISPLAY "  ACTIVE MEMBER " WS-SUB.
           DISPLAY "  END OF CHECK " WS-SUB.

       CONTINUE-CHECK.
           IF MEMBER-SUSPENDED
               CONTINUE
           ELSE
               DISPLAY "  ACTIVE MEMBER " WS-SUB
           END-IF
           DISPLAY "  END OF CHECK " WS-SUB.

Compiled with cobc -x -Wall nextsent.cob. Real output of the compiler, a warning that stops a "no warnings" build, followed by the run:

nextsent.cob: in paragraph 'NEXT-SENTENCE-CHECK':
nextsent.cob:26: warning: NEXT SENTENCE is archaic in GnuCOBOL [-Warchaic]
   24 |        NEXT-SENTENCE-CHECK.
   25 |            IF MEMBER-SUSPENDED
   26 >                NEXT SENTENCE
   27 |            ELSE
   28 |                DISPLAY "  ACTIVE MEMBER " WS-SUB.
NEXT SENTENCE
  ACTIVE MEMBER 1
  END OF CHECK 1
  END OF CHECK 2
  ACTIVE MEMBER 3
  END OF CHECK 3
CONTINUE
  ACTIVE MEMBER 1
  END OF CHECK 1
  END OF CHECK 2
  ACTIVE MEMBER 3
  END OF CHECK 3

The hazard is NEXT SENTENCE in an IF that also has END-IF. The Language Reference says that then control does not pass to the statement after END-IF: it passes to the statement after the closest following period, which can be the end of the paragraph, so the statements between END-IF and the period are skipped. The GnuCOBOL Programmer’s Guide says that NEXT SENTENCE and END-IF cannot be used in the same IF statement. This is why this guide does not use NEXT SENTENCE in new code and does not demonstrate the combination: write CONTINUE for "do nothing here" and restructure with NOT if the empty branch bothers you. GnuCOBOL 3.2.0 warns about NEXT SENTENCE (-Warchaic) under -Wall. The remaining older constructs of this kind are collected in Legacy Features and Migration.

The EVALUATE statement

EVALUATE is COBOL’s multi-way decision: a shorthand for a series of nested `IF`s that can test several things at once. It is the right tool for decision tables.

       EVALUATE subject [ALSO subject] ...
           WHEN object [ALSO object] ...
               imperative-statement ...
           [WHEN object [ALSO object] ...
               imperative-statement ...] ...
           [WHEN OTHER
               imperative-statement ...]
       END-EVALUATE

The operands between EVALUATE and the first WHEN are the selection subjects, separated by ALSO. Each WHEN has one selection object for every subject, separated by ALSO, and the objects are matched with the subjects by position. The rules:

  • A subject is an identifier, a literal, an arithmetic expression, a conditional expression (a condition), or the words TRUE or FALSE.

  • An object is an identifier, a literal or an arithmetic expression (compared for equality with the subject), a range value-1 THRU value-2 (both ends of the same class; the subject matches if it is within the range, ends included; a range whose first operand is greater than the second matches nothing), a condition, TRUE or FALSE, or ANY, which matches whatever the subject is. NOT in front of a value or a range matches everything but it.

  • Subject and object kinds must correspond: a condition or TRUE/FALSE as an object needs a conditional expression (or TRUE/FALSE) as the subject in the same position.

  • The WHEN phrases are tried in the order written and the first one whose objects are all satisfied is selected. There is no fall-through to the next WHEN after its statements, so no break is needed, and order is the way to give one rule priority over another.

  • Several WHEN phrases can share one set of statements: a WHEN with no statement of its own uses the statements of the next WHEN that has some (the "O" ALSO "A" and "o" ALSO "A" phrases below).

  • WHEN OTHER runs when no WHEN matched. If it is missing and nothing matched, execution continues after END-EVALUATE and nothing happens, which is rarely what you want for a business rule; code WHEN OTHER and make it report.

  • The number of objects in each WHEN must be the same as the number of subjects (the compiler checks it; see the diagnostics below).

Example: values, ranges, ALSO, TRUE and FALSE

       IDENTIFICATION DIVISION.
       PROGRAM-ID. EVALFORMS.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
           COPY MEMBREC.
           COPY LOANREC.
       01  WS-FINE-VALUES.
           05  FILLER              PIC 9(5)V99 VALUE 0.
           05  FILLER              PIC 9(5)V99 VALUE 0.01.
           05  FILLER              PIC 9(5)V99 VALUE 9.99.
           05  FILLER              PIC 9(5)V99 VALUE 10.
           05  FILLER              PIC 9(5)V99 VALUE 49.99.
           05  FILLER              PIC 9(5)V99 VALUE 50.
           05  FILLER              PIC 9(5)V99 VALUE 120.
       01  WS-FINE-TABLE REDEFINES WS-FINE-VALUES.
           05  WS-FINE             PIC 9(5)V99 OCCURS 7 TIMES.
       01  WS-CASE-VALUES          PIC X(12)   VALUE "OAOSRSRAoAXA".
       01  WS-CASE-TABLE REDEFINES WS-CASE-VALUES.
           05  WS-CASE             OCCURS 6 TIMES.
               10  WS-CASE-TYPE    PIC X.
               10  WS-CASE-STATUS  PIC X.
       01  WS-SUB                  PIC 9       VALUE ZERO.
       01  WS-FINE-EDIT            PIC ZZZZ9.99.
       01  WS-TIER                 PIC X(8).
       01  WS-ACTION               PIC X(24).
       PROCEDURE DIVISION.
       MAIN-PARA.
           PERFORM VARYING WS-SUB FROM 1 BY 1 UNTIL WS-SUB > 7
               MOVE WS-FINE (WS-SUB) TO MEMBER-FINES WS-FINE-EDIT
               EVALUATE MEMBER-FINES
                   WHEN ZERO
                       MOVE "NONE" TO WS-TIER
                   WHEN 0.01 THRU 9.99
                       MOVE "NOTICE" TO WS-TIER
                   WHEN 10 THRU 49.99
                       MOVE "HOLD" TO WS-TIER
                   WHEN OTHER
                       MOVE "SUSPEND" TO WS-TIER
               END-EVALUATE
               DISPLAY "FINES " WS-FINE-EDIT " -> " WS-TIER
           END-PERFORM
           PERFORM VARYING WS-SUB FROM 1 BY 1 UNTIL WS-SUB > 6
               MOVE WS-CASE-TYPE (WS-SUB) TO LOAN-TYPE
               MOVE WS-CASE-STATUS (WS-SUB) TO MEMBER-STATUS
               EVALUATE LOAN-TYPE ALSO MEMBER-STATUS
                   WHEN "O" ALSO "A"
                   WHEN "o" ALSO "A"
                       MOVE "CHECK OUT" TO WS-ACTION
                   WHEN "O" ALSO "S"
                       MOVE "REJECT, MEMBER SUSPENDED" TO WS-ACTION
                   WHEN "R" ALSO ANY
                       MOVE "ACCEPT RETURN" TO WS-ACTION
                   WHEN OTHER
                       MOVE "UNKNOWN TRANSACTION" TO WS-ACTION
               END-EVALUATE
               DISPLAY LOAN-TYPE MEMBER-STATUS " -> " WS-ACTION
           END-PERFORM
           MOVE "R" TO LOAN-TYPE
           EVALUATE TRUE
               WHEN LOAN-CHECKOUT
                   DISPLAY "TRUE: CHECKOUT"
               WHEN LOAN-RETURN
                   DISPLAY "TRUE: RETURN"
               WHEN OTHER
                   DISPLAY "TRUE: OTHER"
           END-EVALUATE
           EVALUATE FALSE
               WHEN LOAN-CHECKOUT
                   DISPLAY "FALSE: NOT A CHECKOUT"
               WHEN OTHER
                   DISPLAY "FALSE: OTHER"
           END-EVALUATE
           STOP RUN.

Output:

FINES     0.00 -> NONE
FINES     0.01 -> NOTICE
FINES     9.99 -> NOTICE
FINES    10.00 -> HOLD
FINES    49.99 -> HOLD
FINES    50.00 -> SUSPEND
FINES   120.00 -> SUSPEND
OA -> CHECK OUT
OS -> REJECT, MEMBER SUSPENDED
RS -> ACCEPT RETURN
RA -> ACCEPT RETURN
oA -> CHECK OUT
XA -> UNKNOWN TRANSACTION
TRUE: RETURN
FALSE: NOT A CHECKOUT
  • The first loop is EVALUATE on one subject, MEMBER-FINES, with a THRU range per fines tier and WHEN OTHER for the rest. A range is a range of the comparison, so 0.01 THRU 9.99 includes every value in between with the decimals the item has, and a value above 49.99 and below 50.00 can only exist if the item has more decimals. Keep the tiers contiguous.

  • The second loop has two subjects joined with ALSO (the transaction type and the member status). "R" ALSO ANY accepts a return for any member; "O" and the lowercase "o" share the checkout statements; XA falls to WHEN OTHER.

  • EVALUATE TRUE matches the first WHEN whose condition is true. It is the form to use when the tests are not all on the same field: each WHEN is a complete condition, and the order sets the priority. EVALUATE FALSE matches the conditions that are false.

GnuCOBOL 3.2.0 also accepts a partial expression as a selection object, a relation with no left operand such as WHEN < 5, and a class or sign condition without its subject; its Programmer’s Guide documents it. The Enterprise COBOL format has no such object. The program below compiles without any diagnostic in the default dialect and with -std=ibm, and prints PLENTY; the portable form is EVALUATE TRUE with WHEN BOOK-COPIES < 5.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. PARTIAL.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
           COPY BOOKREC.
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE 7 TO BOOK-COPIES
           EVALUATE BOOK-COPIES
               WHEN ZERO
                   DISPLAY "NONE"
               WHEN < 5
                   DISPLAY "FEW"
               WHEN OTHER
                   DISPLAY "PLENTY"
           END-EVALUATE
           STOP RUN.

Example: a decision table for loan rules with EVALUATE TRUE ALSO

A decision table has one column for every thing you test and one row for every rule; EVALUATE TRUE ALSO TRUE ALSO TRUE is that table in code. The three subjects are the transaction, the member and the book, and every object is a condition (or ANY for "don’t care"). The rules of the nightly run are, in priority order: a return is always accepted; a checkout is rejected for a suspended member, for a withdrawn book and for a book with no copies; it is held for a member whose fines exceed the limit; any other checkout is accepted. The table has seven rows including WHEN OTHER, and the demonstration drives it with seven cases:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. LOANRULE.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
           COPY BOOKREC.
           COPY MEMBREC.
           COPY LOANREC.
       01  WS-FINE-LIMIT           PIC 9(5)V99 VALUE 10.
       01  WS-CASE-VALUES.
           05  FILLER  PIC X(13)   VALUE "OA0000000A003".
           05  FILLER  PIC X(13)   VALUE "OS0005000A001".
           05  FILLER  PIC X(13)   VALUE "OA0001250A002".
           05  FILLER  PIC X(13)   VALUE "OA0000000W001".
           05  FILLER  PIC X(13)   VALUE "OA0000000A000".
           05  FILLER  PIC X(13)   VALUE "RS0005000A001".
           05  FILLER  PIC X(13)   VALUE "XA0000000A001".
       01  WS-CASE-TABLE REDEFINES WS-CASE-VALUES.
           05  WS-CASE             OCCURS 7 TIMES.
               10  WS-CASE-TYPE    PIC X.
               10  WS-CASE-MEMBER  PIC X.
               10  WS-CASE-FINES   PIC 9(5)V99.
               10  WS-CASE-BOOK    PIC X.
               10  WS-CASE-COPIES  PIC 9(3).
       01  WS-SUB                  PIC 9       VALUE ZERO.
       01  WS-DECISION             PIC X(28).
       PROCEDURE DIVISION.
       MAIN-PARA.
           PERFORM VARYING WS-SUB FROM 1 BY 1 UNTIL WS-SUB > 7
               MOVE WS-CASE-TYPE (WS-SUB) TO LOAN-TYPE
               MOVE WS-CASE-MEMBER (WS-SUB) TO MEMBER-STATUS
               MOVE WS-CASE-FINES (WS-SUB) TO MEMBER-FINES
               MOVE WS-CASE-BOOK (WS-SUB) TO BOOK-STATUS
               MOVE WS-CASE-COPIES (WS-SUB) TO BOOK-COPIES
               PERFORM DECIDE-LOAN
               DISPLAY "CASE " WS-SUB ": " WS-DECISION
           END-PERFORM
           STOP RUN.

       DECIDE-LOAN.
           EVALUATE TRUE ALSO TRUE ALSO TRUE
               WHEN LOAN-RETURN ALSO ANY ALSO ANY
                   MOVE "ACCEPT RETURN" TO WS-DECISION
               WHEN LOAN-CHECKOUT ALSO MEMBER-SUSPENDED ALSO ANY
                   MOVE "REJECT: MEMBER SUSPENDED" TO WS-DECISION
               WHEN LOAN-CHECKOUT ALSO ANY ALSO BOOK-WITHDRAWN
                   MOVE "REJECT: BOOK WITHDRAWN" TO WS-DECISION
               WHEN LOAN-CHECKOUT ALSO ANY ALSO BOOK-COPIES = ZERO
                   MOVE "REJECT: NO COPY AVAILABLE" TO WS-DECISION
               WHEN LOAN-CHECKOUT ALSO MEMBER-FINES > WS-FINE-LIMIT
                       ALSO ANY
                   MOVE "HOLD: FINES OVER LIMIT" TO WS-DECISION
               WHEN LOAN-CHECKOUT ALSO ANY ALSO ANY
                   MOVE "ACCEPT CHECKOUT" TO WS-DECISION
               WHEN OTHER
                   MOVE "REJECT: UNKNOWN TYPE" TO WS-DECISION
           END-EVALUATE.
CASE 1: ACCEPT CHECKOUT
CASE 2: REJECT: MEMBER SUSPENDED
CASE 3: HOLD: FINES OVER LIMIT
CASE 4: REJECT: BOOK WITHDRAWN
CASE 5: REJECT: NO COPY AVAILABLE
CASE 6: ACCEPT RETURN
CASE 7: REJECT: UNKNOWN TYPE

Case 6 is a suspended member who returns a book: the first WHEN accepts it before the suspension rule is ever reached. Case 3 has fines of 12.50 against a limit of 10.00. The rows are in the order of the diagram, which is the order in which the compiler tries them:

flowchart TD START(["EVALUATE TRUE ALSO TRUE ALSO TRUE"]) --> R1{"LOAN-RETURN?"} R1 -- "yes" --> A1["ACCEPT RETURN"] R1 -- "no" --> R2{"LOAN-CHECKOUT and MEMBER-SUSPENDED?"} R2 -- "yes" --> X1["REJECT: MEMBER SUSPENDED"] R2 -- "no" --> R3{"LOAN-CHECKOUT and BOOK-WITHDRAWN?"} R3 -- "yes" --> X2["REJECT: BOOK WITHDRAWN"] R3 -- "no" --> R4{"LOAN-CHECKOUT and BOOK-COPIES = ZERO?"} R4 -- "yes" --> X3["REJECT: NO COPY AVAILABLE"] R4 -- "no" --> R5{"LOAN-CHECKOUT and fines over limit?"} R5 -- "yes" --> H1["HOLD: FINES OVER LIMIT"] R5 -- "no" --> R6{"LOAN-CHECKOUT?"} R6 -- "yes" --> A2["ACCEPT CHECKOUT"] R6 -- "no" --> O1["WHEN OTHER: REJECT, UNKNOWN TYPE"]

The diagram shows the decision order of the EVALUATE: each diamond is one WHEN phrase, the first test that passes picks the action, and WHEN OTHER is what remains. The ANY objects are the conditions missing from a diamond. Reordering the phrases changes the answer for a suspended member who returns a book, so the order is part of the specification. A comment next to the statement, or a test for each row, is how that specification is kept (see Coding Standards and Best Practices).

Pitfalls, with the real diagnostics

The program below is deliberately wrong. It contains the errors that the compiler does catch:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. BADCOND.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
           COPY BOOKREC.
           COPY LOANREC.
       PROCEDURE DIVISION.
       MAIN-PARA.
           IF "A" = "B"
               DISPLAY "C"
           END-IF
           EVALUATE LOAN-TYPE ALSO BOOK-STATUS
               WHEN "O"
                   DISPLAY "D"
           END-EVALUATE
           EVALUATE BOOK-COPIES
               WHEN "A" THRU 9
                   DISPLAY "E"
           END-EVALUATE
           IF BOOK-COPIES NOT > 5 AND NOT <
               DISPLAY "F"
           END-IF
           STOP RUN.

Real output of cobc -x -Wall bad.cob:

bad.cob: in paragraph 'MAIN-PARA':
bad.cob:9: warning: expression 'A' EQUALS 'B' is always FALSE [-Wconstant-expression]
    7 |        PROCEDURE DIVISION.
    8 |        MAIN-PARA.
    9 >            IF "A" = "B"
   10 |                DISPLAY "C"
   11 |            END-IF
bad.cob:13: error: wrong number of WHEN parameters
   11 |            END-IF
   12 |            EVALUATE LOAN-TYPE ALSO BOOK-STATUS
   13 >                WHEN "O"
   14 |                    DISPLAY "D"
   15 |            END-EVALUATE
bad.cob:17: warning: literal 'A' is alphanumeric but 'BOOK-COPIES' is numeric [-Wconstant-expression]
   15 |            END-EVALUATE
   16 |            EVALUATE BOOK-COPIES
   17 >                WHEN "A" THRU 9
   18 |                    DISPLAY "E"
   19 |            END-EVALUATE
bad.cob:20: error: invalid expression: unfinished expression
   18 |                    DISPLAY "E"
   19 |            END-EVALUATE
   20 >            IF BOOK-COPIES NOT > 5 AND NOT <
   21 |                DISPLAY "F"
   22 |            END-IF
  • IF "A" = "B" compares two literals; the answer is known at compile time, and the compiler says so. Such tests are usually left over from debugging or from a copybook expansion.

  • An EVALUATE with two subjects and a WHEN with one object is an error: the objects must match the subjects one to one.

  • WHEN "A" THRU 9 mixes classes. Here the compiler warns and the test can never be true; Enterprise COBOL requires the two operands of a THRU to be of the same class.

  • NOT > with no operand is an incomplete expression, an error.

What the compiler does not catch matters more: the stray period, the abbreviated NOT = 1 OR 2, an ALPHABETIC test on a numeric item, = 12.5 against an alphanumeric item, and a numeric class test on data that was never validated. The first three have examples above; the rest need review and test cases, as does the rule of every decision table, which is one test per row.

Where the book is outdated

The book consulted for this section (Taulli, Modern Mainframe Development, 2022) was written around Enterprise COBOL 6.3 and covers IF, class conditions and EVALUATE at the survey level it gives the rest of the language; nothing is copied from it. What a reader should update, using the current manuals:

  • The OMITTED class test and the OPTIONAL phrase on BY REFERENCE parameters are documented by the 6.5 Language Reference; the section outline for this site lists them as new in 6.5, a release the book (6.3) predates. The book could not describe them.

  • The book’s examples were run with GnuCOBOL (OpenCobolIDE), which accepts several things that Enterprise COBOL rejects or that compile only with an option, as shown above: ALPHABETIC on a numeric item, a non-integer literal compared with an alphanumeric item, and partial expressions in EVALUATE. A test that passes on a PC can fail the z/OS compile.

  • COBOL has moved to explicit scope terminators since 1985. The period-terminated nested IF and NEXT SENTENCE that old examples (and programs) use survive for compatibility only; new code uses END-IF, END-EVALUATE and CONTINUE.