29 min read

> "The measure of a good program is not how cleverly it solves the problem, but how clearly it communicates the solution."

Chapter 6: Advanced Conditional Logic

"The measure of a good program is not how cleverly it solves the problem, but how clearly it communicates the solution." — Gerald Weinberg

Every program makes decisions. A payroll system decides whether an employee is salaried or hourly. A banking application decides whether to approve or decline a transaction. An insurance processor decides which benefit schedule applies to a claim. The quality of your conditional logic — its correctness, its readability, its maintainability — determines the quality of your program.

In your first COBOL course, you learned the basics: the IF statement, simple conditions, and perhaps a taste of EVALUATE. In this chapter, we go far deeper. You will learn to write conditional logic that is not only correct but readable — logic that a maintenance programmer encountering your code five years from now will understand without needing to trace through it line by line.

This is where the theme Readability is a Feature becomes concrete. In the world of enterprise COBOL, where programs live for decades and are maintained by dozens of programmers over their lifetimes, the readability of your conditional logic is not a luxury. It is a survival strategy.


6.1 The Anatomy of a COBOL Condition

Before we dive into advanced patterns, let us establish precise terminology. In COBOL, a condition is an expression that evaluates to either true or false. The COBOL standard defines several categories of conditions:

Condition Type Example
Relation condition IF WS-BALANCE > 0
Class condition IF WS-AMOUNT IS NUMERIC
Sign condition IF WS-BALANCE IS POSITIVE
Condition-name condition IF ACCT-IS-ACTIVE
Complex condition IF WS-AGE > 18 AND WS-STATUS = 'A'
Negated condition IF NOT ACCT-IS-CLOSED

Every conditional statement in COBOL ultimately resolves to one of these types or a combination of them. Understanding the taxonomy helps you choose the right tool for each situation.

Relation Conditions

A relation condition compares two operands using a relational operator:

       IF WS-ACCOUNT-BALANCE > WS-WITHDRAWAL-AMOUNT
           PERFORM 2100-PROCESS-WITHDRAWAL
       ELSE
           PERFORM 2200-INSUFFICIENT-FUNDS
       END-IF

COBOL provides the following relational operators:

Symbol Word Form Meaning
> IS GREATER THAN Greater than
< IS LESS THAN Less than
= IS EQUAL TO Equal to
>= IS GREATER THAN OR EQUAL TO Greater than or equal
<= IS LESS THAN OR EQUAL TO Less than or equal
NOT = IS NOT EQUAL TO Not equal

💡 Style Tip: Many shops mandate one form over the other. Some prefer symbols for brevity (>, <, =), while others prefer word forms for readability (IS GREATER THAN). The important thing is consistency within a project. At GlobalBank, Maria Chen's team uses symbols for simple comparisons and word forms when they make compound conditions clearer.


6.2 Nested IF Statements — Depth Management

Nested IF statements are one of the most common — and most dangerous — constructs in COBOL. They are common because business rules are naturally hierarchical: "If the account is active, and if the balance is sufficient, and if the transaction type is valid, then process the transaction." They are dangerous because deep nesting quickly becomes unreadable.

The Nesting Problem

Consider this code from GlobalBank's early transaction processing module, written in 1992:

       IF WS-ACCT-STATUS = 'A'
           IF WS-TXN-TYPE = 'W'
               IF WS-BALANCE >= WS-TXN-AMOUNT
                   IF WS-DAILY-LIMIT >= WS-TXN-AMOUNT
                       IF WS-ACCT-HOLD = 'N'
                           PERFORM 3100-PROCESS-WITHDRAWAL
                       ELSE
                           MOVE 'ACCOUNT ON HOLD'
                               TO WS-ERROR-MSG
                           PERFORM 9100-LOG-ERROR
                       END-IF
                   ELSE
                       MOVE 'DAILY LIMIT EXCEEDED'
                           TO WS-ERROR-MSG
                       PERFORM 9100-LOG-ERROR
                   END-IF
               ELSE
                   MOVE 'INSUFFICIENT FUNDS'
                       TO WS-ERROR-MSG
                   PERFORM 9100-LOG-ERROR
               END-IF
           ELSE
               IF WS-TXN-TYPE = 'D'
                   PERFORM 3200-PROCESS-DEPOSIT
               ELSE
                   IF WS-TXN-TYPE = 'T'
                       PERFORM 3300-PROCESS-TRANSFER
                   ELSE
                       MOVE 'INVALID TXN TYPE'
                           TO WS-ERROR-MSG
                       PERFORM 9100-LOG-ERROR
                   END-IF
               END-IF
           END-IF
       ELSE
           MOVE 'ACCOUNT NOT ACTIVE'
               TO WS-ERROR-MSG
           PERFORM 9100-LOG-ERROR
       END-IF

This is five levels deep in places. When Derek Washington first encountered this code, he spent twenty minutes tracing which ELSE matched which IF. And this is a simple example — production code at GlobalBank sometimes nests eight or nine levels deep.

⚠️ Warning: Most coding standards limit nesting to three or four levels. Beyond that, the human brain struggles to maintain the mental stack of conditions. If you find yourself nesting deeper than three levels, it is time to refactor.

Strategy 1: Early Exit (Guard Clause Pattern)

The most effective way to reduce nesting is to check for error conditions first and exit early:

       IF WS-ACCT-STATUS NOT = 'A'
           MOVE 'ACCOUNT NOT ACTIVE' TO WS-ERROR-MSG
           PERFORM 9100-LOG-ERROR
           GO TO 2000-PROCESS-TXN-EXIT
       END-IF

       IF WS-TXN-TYPE = 'D'
           PERFORM 3200-PROCESS-DEPOSIT
           GO TO 2000-PROCESS-TXN-EXIT
       END-IF

       IF WS-TXN-TYPE = 'T'
           PERFORM 3300-PROCESS-TRANSFER
           GO TO 2000-PROCESS-TXN-EXIT
       END-IF

       IF WS-TXN-TYPE NOT = 'W'
           MOVE 'INVALID TXN TYPE' TO WS-ERROR-MSG
           PERFORM 9100-LOG-ERROR
           GO TO 2000-PROCESS-TXN-EXIT
       END-IF

      * At this point we know: account is active, txn is withdrawal
       IF WS-BALANCE < WS-TXN-AMOUNT
           MOVE 'INSUFFICIENT FUNDS' TO WS-ERROR-MSG
           PERFORM 9100-LOG-ERROR
           GO TO 2000-PROCESS-TXN-EXIT
       END-IF

       IF WS-DAILY-LIMIT < WS-TXN-AMOUNT
           MOVE 'DAILY LIMIT EXCEEDED' TO WS-ERROR-MSG
           PERFORM 9100-LOG-ERROR
           GO TO 2000-PROCESS-TXN-EXIT
       END-IF

       IF WS-ACCT-HOLD = 'Y'
           MOVE 'ACCOUNT ON HOLD' TO WS-ERROR-MSG
           PERFORM 9100-LOG-ERROR
           GO TO 2000-PROCESS-TXN-EXIT
       END-IF

       PERFORM 3100-PROCESS-WITHDRAWAL
       .
       2000-PROCESS-TXN-EXIT.
           EXIT.

⚖️ Debate: This pattern uses GO TO — which some shops ban entirely. We will explore this controversy in depth in Chapter 8. For now, notice that the guard clause pattern, even with GO TO, produces code that reads like a checklist: each condition is checked independently, each error is handled immediately, and the "happy path" (the withdrawal) only executes if all checks pass. Many experienced COBOL programmers consider this a legitimate, even preferred, use of GO TO.

Strategy 2: Condition Flags with Sequential IFs

If your shop prohibits GO TO, you can achieve a similar effect with a processing flag:

       MOVE 'Y' TO WS-OK-TO-PROCESS

       IF WS-ACCT-STATUS NOT = 'A'
           MOVE 'ACCOUNT NOT ACTIVE' TO WS-ERROR-MSG
           PERFORM 9100-LOG-ERROR
           MOVE 'N' TO WS-OK-TO-PROCESS
       END-IF

       IF WS-OK-TO-PROCESS = 'Y'
       AND WS-TXN-TYPE = 'W'
           IF WS-BALANCE < WS-TXN-AMOUNT
               MOVE 'INSUFFICIENT FUNDS' TO WS-ERROR-MSG
               PERFORM 9100-LOG-ERROR
               MOVE 'N' TO WS-OK-TO-PROCESS
           END-IF
       END-IF

       IF WS-OK-TO-PROCESS = 'Y'
       AND WS-TXN-TYPE = 'W'
           IF WS-DAILY-LIMIT < WS-TXN-AMOUNT
               MOVE 'DAILY LIMIT EXCEEDED' TO WS-ERROR-MSG
               PERFORM 9100-LOG-ERROR
               MOVE 'N' TO WS-OK-TO-PROCESS
           END-IF
       END-IF

       IF WS-OK-TO-PROCESS = 'Y'
           EVALUATE WS-TXN-TYPE
               WHEN 'W' PERFORM 3100-PROCESS-WITHDRAWAL
               WHEN 'D' PERFORM 3200-PROCESS-DEPOSIT
               WHEN 'T' PERFORM 3300-PROCESS-TRANSFER
               WHEN OTHER
                   MOVE 'INVALID TXN TYPE' TO WS-ERROR-MSG
                   PERFORM 9100-LOG-ERROR
           END-EVALUATE
       END-IF

This avoids GO TO at the cost of repeatedly checking WS-OK-TO-PROCESS. Some programmers find this cleaner; others find the repeated flag checks verbose. Both patterns are valid — what matters is that your team agrees on a standard.

Strategy 3: Extract to Paragraphs

The most universally accepted strategy is to move nested logic into well-named paragraphs:

       2000-PROCESS-TRANSACTION.
           PERFORM 2010-VALIDATE-ACCOUNT
           IF WS-VALIDATION-OK = 'Y'
               PERFORM 2020-ROUTE-TRANSACTION
           END-IF
           .

       2010-VALIDATE-ACCOUNT.
           MOVE 'Y' TO WS-VALIDATION-OK
           IF WS-ACCT-STATUS NOT = 'A'
               MOVE 'ACCOUNT NOT ACTIVE' TO WS-ERROR-MSG
               MOVE 'N' TO WS-VALIDATION-OK
           END-IF
           IF WS-VALIDATION-OK = 'Y'
           AND WS-ACCT-HOLD = 'Y'
               MOVE 'ACCOUNT ON HOLD' TO WS-ERROR-MSG
               MOVE 'N' TO WS-VALIDATION-OK
           END-IF
           .

       2020-ROUTE-TRANSACTION.
           EVALUATE WS-TXN-TYPE
               WHEN 'W' PERFORM 2100-VALIDATE-AND-WITHDRAW
               WHEN 'D' PERFORM 3200-PROCESS-DEPOSIT
               WHEN 'T' PERFORM 3300-PROCESS-TRANSFER
               WHEN OTHER
                   MOVE 'INVALID TXN TYPE' TO WS-ERROR-MSG
                   PERFORM 9100-LOG-ERROR
           END-EVALUATE
           .

This approach has the best of all worlds: no GO TO, minimal nesting, and each paragraph has a clear, single responsibility. We will explore this design philosophy extensively in Chapter 8.


6.3 The EVALUATE Statement — COBOL's Decision Table

The EVALUATE statement, introduced in COBOL-85, is one of the language's most powerful and underused constructs. If you come from other languages, you might think of EVALUATE as a "switch" or "case" statement. It is that, but it is also much more. EVALUATE is a full decision table — it can evaluate multiple subjects simultaneously and match against complex combinations of values.

Basic EVALUATE

The simplest form evaluates a single identifier against a list of values:

       EVALUATE WS-TXN-TYPE
           WHEN 'W'
               PERFORM 3100-PROCESS-WITHDRAWAL
           WHEN 'D'
               PERFORM 3200-PROCESS-DEPOSIT
           WHEN 'T'
               PERFORM 3300-PROCESS-TRANSFER
           WHEN 'I'
               PERFORM 3400-PROCESS-INQUIRY
           WHEN OTHER
               MOVE 'INVALID TXN TYPE' TO WS-ERROR-MSG
               PERFORM 9100-LOG-ERROR
       END-EVALUATE

Best Practice: Always include a WHEN OTHER clause. Even if you believe you have covered all possible values, a WHEN OTHER clause catches unexpected data and prevents your program from silently doing nothing — one of the most dangerous bugs in any system.

EVALUATE TRUE — The Power Pattern

EVALUATE TRUE is arguably the most powerful form. Instead of evaluating an identifier against values, it evaluates which condition is true:

       EVALUATE TRUE
           WHEN WS-BALANCE > 100000
               MOVE 'PLATINUM' TO WS-ACCT-TIER
           WHEN WS-BALANCE > 25000
               MOVE 'GOLD' TO WS-ACCT-TIER
           WHEN WS-BALANCE > 5000
               MOVE 'SILVER' TO WS-ACCT-TIER
           WHEN WS-BALANCE > 0
               MOVE 'STANDARD' TO WS-ACCT-TIER
           WHEN OTHER
               MOVE 'OVERDRAWN' TO WS-ACCT-TIER
       END-EVALUATE

⚠️ Critical: EVALUATE evaluates WHEN clauses in order, top to bottom, and executes only the first match. This matters enormously with EVALUATE TRUE. In the example above, a balance of $150,000 matches both > 100000 and > 25000, but only the PLATINUM assignment executes because it comes first. Always order your WHEN clauses from most specific to most general.

EVALUATE TRUE replaces deeply nested IF-ELSE chains. Consider this common MedClaim pattern for determining claim priority:

      * BEFORE: Nested IF (hard to read)
       IF CLM-TYPE = 'E'
           MOVE 1 TO WS-PRIORITY
       ELSE
           IF CLM-AMOUNT > 50000
               MOVE 2 TO WS-PRIORITY
           ELSE
               IF CLM-PROVIDER-TYPE = 'H'
                   MOVE 3 TO WS-PRIORITY
               ELSE
                   MOVE 4 TO WS-PRIORITY
               END-IF
           END-IF
       END-IF

      * AFTER: EVALUATE TRUE (reads like a decision table)
       EVALUATE TRUE
           WHEN CLM-TYPE = 'E'
               MOVE 1 TO WS-PRIORITY
           WHEN CLM-AMOUNT > 50000
               MOVE 2 TO WS-PRIORITY
           WHEN CLM-PROVIDER-TYPE = 'H'
               MOVE 3 TO WS-PRIORITY
           WHEN OTHER
               MOVE 4 TO WS-PRIORITY
       END-EVALUATE

The EVALUATE version is flatter, each case is visually parallel, and the logic is immediately apparent. This is what we mean by Readability is a Feature.

EVALUATE with Multiple Subjects (ALSO)

The ALSO clause enables EVALUATE to act as a true two-dimensional (or multi-dimensional) decision table. This is where EVALUATE becomes uniquely powerful — no other mainstream language has a built-in construct this expressive.

       EVALUATE WS-ACCT-TYPE ALSO WS-TXN-TYPE
           WHEN 'C' ALSO 'W'
               PERFORM 3110-CHECKING-WITHDRAWAL
           WHEN 'C' ALSO 'D'
               PERFORM 3120-CHECKING-DEPOSIT
           WHEN 'S' ALSO 'W'
               PERFORM 3210-SAVINGS-WITHDRAWAL
           WHEN 'S' ALSO 'D'
               PERFORM 3220-SAVINGS-DEPOSIT
           WHEN 'M' ALSO ANY
               PERFORM 3300-MONEY-MARKET-TXN
           WHEN ANY ALSO 'I'
               PERFORM 3400-INQUIRY
           WHEN OTHER
               MOVE 'INVALID ACCT/TXN COMBINATION'
                   TO WS-ERROR-MSG
               PERFORM 9100-LOG-ERROR
       END-EVALUATE

Notice the use of ANY as a wildcard. WHEN 'M' ALSO ANY means "money market account with any transaction type." WHEN ANY ALSO 'I' means "any account type with an inquiry transaction." This is a decision table brought to life in code.

📊 Decision Table Mapping: Here is the conceptual decision table that the EVALUATE above implements:

W (Withdrawal) D (Deposit) I (Inquiry) Other
C (Checking) 3110 3120 3400 Error
S (Savings) 3210 3220 3400 Error
M (Money Mkt) 3300 3300 3300* Error
Other Error Error Error Error

*Note: The Money Market row catches ALL transaction types via ANY, including Inquiry. But because WHEN 'M' ALSO ANY appears before WHEN ANY ALSO 'I' in the code, money market inquiries go to 3300 rather than 3400. Order matters!

EVALUATE TRUE ALSO TRUE

You can combine EVALUATE TRUE with ALSO to create multi-dimensional condition tables:

       EVALUATE TRUE ALSO TRUE
           WHEN WS-AGE < 18    ALSO WS-GUARDIAN = 'Y'
               PERFORM 4100-MINOR-WITH-GUARDIAN
           WHEN WS-AGE < 18    ALSO WS-GUARDIAN = 'N'
               PERFORM 4200-MINOR-NO-GUARDIAN
           WHEN WS-AGE >= 65   ALSO WS-RETIRED = 'Y'
               PERFORM 4300-SENIOR-RETIRED
           WHEN WS-AGE >= 65   ALSO WS-RETIRED = 'N'
               PERFORM 4400-SENIOR-WORKING
           WHEN OTHER
               PERFORM 4500-STANDARD-ADULT
       END-EVALUATE

This is essentially a two-dimensional truth table expressed directly in code. At GlobalBank, Maria Chen uses this pattern extensively for fee calculation logic where both account tier and transaction characteristics determine the fee schedule.

EVALUATE with Ranges and NOT

WHEN clauses can specify ranges and negations:

       EVALUATE WS-CREDIT-SCORE
           WHEN 750 THRU 850
               MOVE 'EXCELLENT' TO WS-CREDIT-TIER
           WHEN 700 THRU 749
               MOVE 'GOOD' TO WS-CREDIT-TIER
           WHEN 650 THRU 699
               MOVE 'FAIR' TO WS-CREDIT-TIER
           WHEN 300 THRU 649
               MOVE 'POOR' TO WS-CREDIT-TIER
           WHEN OTHER
               MOVE 'INVALID SCORE' TO WS-ERROR-MSG
               PERFORM 9100-LOG-ERROR
       END-EVALUATE

The THRU keyword makes range checks clean and readable. Compare this with the equivalent nested IF:

      * Equivalent nested IF — much harder to read
       IF WS-CREDIT-SCORE >= 750 AND WS-CREDIT-SCORE <= 850
           MOVE 'EXCELLENT' TO WS-CREDIT-TIER
       ELSE IF WS-CREDIT-SCORE >= 700
           MOVE 'GOOD' TO WS-CREDIT-TIER
       ELSE IF WS-CREDIT-SCORE >= 650
           MOVE 'FAIR' TO WS-CREDIT-TIER
       ELSE IF WS-CREDIT-SCORE >= 300
           MOVE 'POOR' TO WS-CREDIT-TIER
       ELSE
           MOVE 'INVALID SCORE' TO WS-ERROR-MSG
           PERFORM 9100-LOG-ERROR
       END-IF

The EVALUATE version is not only more readable — it is also safer, because each range is explicitly bounded. In the nested IF version, a score of 299 would fall through to the ELSE, which might be correct or might be a bug depending on the business rule.

🧪 Try It Yourself: EVALUATE Decision Table

Open your Student Mainframe Lab (or GnuCOBOL environment) and write a program that implements a shipping cost calculator using EVALUATE with ALSO:

  • Subject 1: WS-SHIPPING-ZONE (values: 'D' domestic, 'I' international)
  • Subject 2: WS-PACKAGE-SIZE (values: 'S' small, 'M' medium, 'L' large)
  • Subject 3: WS-PRIORITY (values: 'S' standard, 'E' express)

Create the EVALUATE statement that sets WS-SHIPPING-COST based on all three dimensions. Use at least one ANY wildcard. Include a WHEN OTHER for invalid combinations.


6.4 Condition Names (88-Level Items) — Boolean Abstractions

If there is one COBOL feature that, when mastered, will improve your code more than any other, it is the 88-level condition name. Condition names transform cryptic value comparisons into self-documenting boolean expressions. They are one of COBOL's most elegant features and one that many other languages still lack.

Basic Condition Names

An 88-level item is defined subordinate to a data item and associates a meaningful name with one or more values of that item:

       01  WS-ACCOUNT-STATUS        PIC X(01).
           88  ACCT-IS-ACTIVE        VALUE 'A'.
           88  ACCT-IS-CLOSED        VALUE 'C'.
           88  ACCT-IS-SUSPENDED     VALUE 'S'.
           88  ACCT-IS-PENDING       VALUE 'P'.

Now instead of writing:

       IF WS-ACCOUNT-STATUS = 'A'

You write:

       IF ACCT-IS-ACTIVE

This might seem like a minor improvement, but consider the impact at scale. A maintenance programmer reading IF ACCT-IS-ACTIVE understands the intent immediately. A programmer reading IF WS-ACCOUNT-STATUS = 'A' has to know (or look up) what 'A' means. When you have programs with hundreds of conditionals, this difference is enormous.

Multiple Values

An 88-level can represent multiple values:

       01  WS-TXN-TYPE              PIC X(02).
           88  TXN-IS-DEBIT         VALUE 'WD' 'PY' 'FE'.
           88  TXN-IS-CREDIT        VALUE 'DP' 'CR' 'RF'.
           88  TXN-IS-INQUIRY       VALUE 'IQ' 'BQ' 'SQ'.
           88  TXN-IS-VALID         VALUE 'WD' 'PY' 'FE'
                                          'DP' 'CR' 'RF'
                                          'IQ' 'BQ' 'SQ'.

Now IF TXN-IS-DEBIT is true if the transaction type is 'WD' (withdrawal), 'PY' (payment), or 'FE' (fee). This is vastly more readable than:

       IF WS-TXN-TYPE = 'WD' OR 'PY' OR 'FE'

THRU (Range Values)

Condition names can also specify ranges:

       01  WS-CREDIT-SCORE          PIC 9(03).
           88  CREDIT-EXCELLENT      VALUE 750 THRU 850.
           88  CREDIT-GOOD           VALUE 700 THRU 749.
           88  CREDIT-FAIR           VALUE 650 THRU 699.
           88  CREDIT-POOR           VALUE 300 THRU 649.
           88  CREDIT-VALID          VALUE 300 THRU 850.

The EVALUATE from the previous section becomes:

       EVALUATE TRUE
           WHEN CREDIT-EXCELLENT
               MOVE 'EXCELLENT' TO WS-CREDIT-TIER
           WHEN CREDIT-GOOD
               MOVE 'GOOD' TO WS-CREDIT-TIER
           WHEN CREDIT-FAIR
               MOVE 'FAIR' TO WS-CREDIT-TIER
           WHEN CREDIT-POOR
               MOVE 'POOR' TO WS-CREDIT-TIER
           WHEN OTHER
               MOVE 'INVALID SCORE' TO WS-ERROR-MSG
               PERFORM 9100-LOG-ERROR
       END-EVALUATE

This is the gold standard of readable conditional logic: EVALUATE TRUE combined with well-named 88-level conditions. The code reads almost like English.

The SET Statement

You can set a condition name to true using the SET statement:

       SET ACCT-IS-ACTIVE TO TRUE

This is equivalent to MOVE 'A' TO WS-ACCOUNT-STATUS, but it is self-documenting and, crucially, it insulates your logic from the underlying representation. If the value for "active" ever changes from 'A' to '1', you only change it in the 88-level definition — every SET statement and every IF statement using the condition name continues to work without modification.

💡 Design Insight: This is the principle of abstraction applied to data values. The 88-level condition name creates a layer of indirection between the logical concept ("the account is active") and the physical representation (the character 'A' in a one-byte field). This indirection is not overhead — it is insurance.

FALSE Values (COBOL 2002+)

In COBOL 2002 and later, you can specify a FALSE value for an 88-level:

       01  WS-PROCESSING-FLAG       PIC X(01).
           88  IS-PROCESSING         VALUE 'Y'
                                     FALSE IS 'N'.

This allows:

       SET IS-PROCESSING TO TRUE     *> Sets to 'Y'
       SET IS-PROCESSING TO FALSE    *> Sets to 'N'

⚠️ Compatibility Note: The FALSE clause is a COBOL 2002 feature. If your shop uses an older compiler standard, you may not have this available. Check your compiler documentation.

Condition Name Hierarchies

You can create hierarchies of condition names for different levels of specificity:

       01  WS-CLAIM-STATUS          PIC X(02).
           88  CLM-NEW               VALUE 'NW'.
           88  CLM-IN-REVIEW         VALUE 'IR'.
           88  CLM-PENDING-INFO      VALUE 'PI'.
           88  CLM-APPROVED          VALUE 'AP'.
           88  CLM-DENIED            VALUE 'DN'.
           88  CLM-APPEALED          VALUE 'AL'.
           88  CLM-PAID              VALUE 'PD'.
      *    Group conditions
           88  CLM-IS-OPEN           VALUE 'NW' 'IR' 'PI' 'AL'.
           88  CLM-IS-CLOSED         VALUE 'AP' 'DN' 'PD'.
           88  CLM-NEEDS-ACTION      VALUE 'NW' 'PI' 'AL'.
           88  CLM-IS-TERMINAL       VALUE 'DN' 'PD'.

At MedClaim, James Okafor's team uses this pattern extensively. IF CLM-IS-OPEN is far more maintainable than listing out all four status codes every time you need to check whether a claim is still active. And when a new status code is added (say, 'ES' for "escalated"), you update the 88-level definitions in one place — every conditional that uses the group condition automatically includes the new status.

📊 Real-World Impact: Sarah Kim, MedClaim's business analyst, estimates that their migration from raw value comparisons to 88-level condition names reduced claims-processing bugs by roughly 40%. The most common bug type — forgetting to include a new status code in one of many IF statements scattered through the program — was virtually eliminated.

🧪 Try It Yourself: 88-Level Refactoring

Take this poorly written conditional block and refactor it using 88-level condition names:

       IF WS-EMP-TYPE = 'F' OR WS-EMP-TYPE = 'P'
           IF WS-DEPT-CODE = 'IT' OR WS-DEPT-CODE = 'EN'
              OR WS-DEPT-CODE = 'SC'
               IF WS-YRS-SERVICE > 5
                   MOVE 'Y' TO WS-ELIGIBLE
               END-IF
           END-IF
       END-IF

Define 88-levels for employee types (full-time, part-time, contractor), department groups (technical, administrative), and service thresholds. Rewrite the conditional using your 88-level names combined with EVALUATE TRUE.


6.5 Complex Conditions — AND, OR, NOT

Real business rules rarely involve single conditions. COBOL provides the standard boolean operators — AND, OR, and NOT — for combining conditions.

Operator Precedence

COBOL evaluates complex conditions in this order:

  1. NOT (highest precedence — evaluated first)
  2. AND
  3. OR (lowest precedence — evaluated last)

This means:

       IF A OR B AND C

is evaluated as:

       IF A OR (B AND C)

Not as (A OR B) AND C. This trips up many programmers, especially those coming from languages where the precedence might differ or where parentheses are used more habitually.

⚠️ Warning: Always use parentheses to make your intent explicit. Even if you know the precedence rules perfectly, the next programmer to read your code may not. Write:

       IF (A OR B) AND C

or:

       IF A OR (B AND C)

Never rely on implicit precedence in production code.

Implied Subjects and Relational Operators

COBOL allows you to abbreviate compound conditions using implied subjects:

      * Full form
       IF WS-STATE = 'NY' OR WS-STATE = 'NJ' OR WS-STATE = 'CT'

      * Abbreviated form (implied subject)
       IF WS-STATE = 'NY' OR 'NJ' OR 'CT'

Both forms are equivalent. The implied subject carries the last explicitly stated subject (WS-STATE) and the last relational operator (=) forward. This also works with other operators:

      * Full form
       IF WS-AMOUNT > 100 AND WS-AMOUNT < 500

      * Abbreviated form (implied subject AND operator)
       IF WS-AMOUNT > 100 AND < 500

💡 Style Debate: Implied subjects are controversial. Some shops love them for brevity. Others ban them because they can be confusing in complex expressions. Consider:

       IF WS-STATUS = 'A' OR 'B' AND WS-TYPE = 'X'

Is this (STATUS = 'A' OR 'B') AND TYPE = 'X'? Or is it STATUS = 'A' OR (STATUS = 'B' AND TYPE = 'X')? The answer is the second interpretation, because AND has higher precedence. But many programmers would read it as the first. Use parentheses!

Negating Complex Conditions

The NOT operator can negate an entire condition:

       IF NOT (WS-STATUS = 'A' AND WS-TYPE = 'C')

By De Morgan's law, this is equivalent to:

       IF WS-STATUS NOT = 'A' OR WS-TYPE NOT = 'C'

Both are correct, but the first is generally clearer because it expresses the intent as a single negated compound condition rather than two separate conditions linked by OR.

Condition Names in Complex Conditions

This is where 88-levels truly shine in complex conditions:

      * Without 88-levels (hard to read)
       IF (WS-ACCT-STATUS = 'A' OR WS-ACCT-STATUS = 'P')
           AND (WS-TXN-TYPE = 'WD' OR WS-TXN-TYPE = 'PY'
                OR WS-TXN-TYPE = 'FE')
           AND WS-BALANCE >= WS-TXN-AMOUNT

      * With 88-levels (reads like a sentence)
       IF (ACCT-IS-ACTIVE OR ACCT-IS-PENDING)
           AND TXN-IS-DEBIT
           AND WS-BALANCE >= WS-TXN-AMOUNT

The second version is immediately comprehensible. This is the coding style that Maria Chen insists on at GlobalBank — conditions that read like business rules, not like hex dumps.


6.6 Class Conditions

Class conditions test whether a data item's contents belong to a particular category:

       IF WS-AMOUNT IS NUMERIC
           COMPUTE WS-TAX = WS-AMOUNT * WS-TAX-RATE
       ELSE
           MOVE 'INVALID AMOUNT' TO WS-ERROR-MSG
           PERFORM 9100-LOG-ERROR
       END-IF

Built-in Class Conditions

Condition True When
IS NUMERIC Contains only digits (and optional sign for signed fields)
IS ALPHABETIC Contains only letters A-Z, a-z, and spaces
IS ALPHABETIC-LOWER Contains only lowercase letters and spaces
IS ALPHABETIC-UPPER Contains only uppercase letters and spaces
IS NOT NUMERIC Fails the NUMERIC test

⚠️ Critical Gotcha: The NUMERIC test behaves differently depending on the data item's PICTURE. For a PIC X field, IS NUMERIC tests whether all characters are digits 0-9. For a PIC S9 signed numeric field, the test also allows the sign character. For a PIC 9 unsigned numeric field, only digits pass. Always know the PICTURE of the field you are testing.

Defensive Programming with Class Conditions

At GlobalBank, incoming transaction data arrives from multiple sources — ATMs, mobile apps, branch terminals, and batch feeds from partner institutions. Not all sources validate data before sending it. This makes class conditions essential for defensive programming:

       2100-VALIDATE-TXN-AMOUNT.
           IF WS-TXN-AMOUNT-X IS NOT NUMERIC
               MOVE 'NON-NUMERIC AMOUNT' TO WS-ERROR-MSG
               MOVE 'E' TO WS-ERROR-SEVERITY
               PERFORM 9100-LOG-ERROR
               MOVE ZERO TO WS-TXN-AMOUNT
           ELSE
               MOVE WS-TXN-AMOUNT-X TO WS-TXN-AMOUNT
           END-IF
           .

Note the pattern: test the alphanumeric version of the field (WS-TXN-AMOUNT-X, PIC X) for NUMERIC before moving it to the numeric field (WS-TXN-AMOUNT, PIC 9). Attempting to use a non-numeric value in arithmetic produces an ABEND (S0C7 on IBM mainframes) — the most common production abend in COBOL systems.

🔴 Production War Story: James Okafor at MedClaim recalls a midnight page in 2019: the nightly claims batch had abended with an S0C7. The culprit? A provider had submitted a claim with the letter 'O' (capital letter O) instead of the digit '0' (zero) in the amount field. A single missing NUMERIC check cost the team four hours of emergency remediation and delayed payment processing for 12,000 claims. The fix was one line: IF CLM-AMOUNT-X IS NUMERIC. The lesson: always validate before you calculate.

User-Defined Class Conditions

COBOL allows you to define custom class conditions in the SPECIAL-NAMES paragraph:

       ENVIRONMENT DIVISION.
       CONFIGURATION SECTION.
       SPECIAL-NAMES.
           CLASS VALID-ACCT-CHAR IS
               'A' THRU 'Z'
               '0' THRU '9'
               '-'.

Then in the PROCEDURE DIVISION:

       IF WS-ACCOUNT-ID IS VALID-ACCT-CHAR
           PERFORM 2100-LOOKUP-ACCOUNT
       ELSE
           MOVE 'INVALID CHARS IN ACCOUNT ID'
               TO WS-ERROR-MSG
           PERFORM 9100-LOG-ERROR
       END-IF

This is particularly useful for validating input data against specific allowed character sets — account numbers that must be alphanumeric with hyphens, names that must be alphabetic with spaces and hyphens, etc.


6.7 Sign Conditions

Sign conditions test whether a numeric value is positive, negative, or zero:

       IF WS-BALANCE IS POSITIVE
           PERFORM 3100-NORMAL-PROCESSING
       END-IF

       IF WS-BALANCE IS NEGATIVE
           PERFORM 3200-OVERDRAFT-PROCESSING
       END-IF

       IF WS-BALANCE IS ZERO
           PERFORM 3300-ZERO-BALANCE-PROCESSING
       END-IF

These are equivalent to comparing against zero:

       IF WS-BALANCE > 0    ...   *> Same as IS POSITIVE
       IF WS-BALANCE < 0    ...   *> Same as IS NEGATIVE
       IF WS-BALANCE = 0    ...   *> Same as IS ZERO

The sign condition forms are preferred for readability. IF WS-BALANCE IS NEGATIVE communicates intent more clearly than IF WS-BALANCE < 0.

💡 Note: IS POSITIVE means strictly greater than zero. It does not include zero. Similarly, IS NEGATIVE means strictly less than zero. Zero is its own sign condition: IS ZERO. This is worth remembering, because in everyday speech we might say a zero balance is "not negative," but in COBOL, NOT NEGATIVE includes both positive values and zero.


6.8 Combining Condition Types: A Worked Example

Let us work through a comprehensive example that combines every condition type discussed so far. This example processes insurance premium calculations at MedClaim, where the premium depends on the member's age, plan type, smoking status, and geographic region.

Step 1: Define the Data with 88-Levels

       01  WS-MEMBER-AGE            PIC 9(03).
           88  AGE-CHILD             VALUE 0 THRU 17.
           88  AGE-YOUNG-ADULT       VALUE 18 THRU 29.
           88  AGE-ADULT             VALUE 30 THRU 49.
           88  AGE-MATURE            VALUE 50 THRU 64.
           88  AGE-SENIOR            VALUE 65 THRU 120.
           88  AGE-VALID             VALUE 0 THRU 120.

       01  WS-PLAN-TYPE             PIC X(03).
           88  PLAN-BRONZE           VALUE 'BRZ'.
           88  PLAN-SILVER           VALUE 'SLV'.
           88  PLAN-GOLD             VALUE 'GLD'.
           88  PLAN-PLATINUM         VALUE 'PLT'.
           88  PLAN-IS-VALID         VALUE 'BRZ' 'SLV'
                                           'GLD' 'PLT'.
           88  PLAN-IS-PREMIUM       VALUE 'GLD' 'PLT'.

       01  WS-SMOKING-STATUS        PIC X(01).
           88  IS-SMOKER             VALUE 'Y'.
           88  IS-NON-SMOKER         VALUE 'N'.
           88  SMOKING-STATUS-VALID  VALUE 'Y' 'N'.

       01  WS-REGION-CODE           PIC X(02).
           88  REGION-NORTHEAST      VALUE 'NE'.
           88  REGION-SOUTHEAST      VALUE 'SE'.
           88  REGION-MIDWEST        VALUE 'MW'.
           88  REGION-WEST           VALUE 'WS'.
           88  REGION-IS-VALID       VALUE 'NE' 'SE' 'MW' 'WS'.
           88  REGION-HIGH-COST      VALUE 'NE' 'WS'.

       01  WS-BASE-PREMIUM          PIC S9(07)V99.
       01  WS-FINAL-PREMIUM         PIC S9(07)V99.
       01  WS-AGE-FACTOR            PIC 9V999.
       01  WS-SMOKING-FACTOR        PIC 9V999.
       01  WS-REGION-FACTOR         PIC 9V999.

Step 2: Validate All Inputs with Class and Condition Checks

       3000-VALIDATE-PREMIUM-INPUTS.
           SET INPUTS-ARE-VALID TO TRUE

      *    Validate age is numeric and in range
           IF WS-MEMBER-AGE-X IS NOT NUMERIC
               SET INPUTS-ARE-INVALID TO TRUE
               MOVE 'AGE IS NOT NUMERIC' TO WS-ERROR-MSG
               PERFORM 9100-LOG-ERROR
           ELSE
               IF NOT AGE-VALID
                   SET INPUTS-ARE-INVALID TO TRUE
                   MOVE 'AGE OUT OF RANGE' TO WS-ERROR-MSG
                   PERFORM 9100-LOG-ERROR
               END-IF
           END-IF

      *    Validate plan type
           IF INPUTS-ARE-VALID
               IF NOT PLAN-IS-VALID
                   SET INPUTS-ARE-INVALID TO TRUE
                   MOVE 'INVALID PLAN TYPE' TO WS-ERROR-MSG
                   PERFORM 9100-LOG-ERROR
               END-IF
           END-IF

      *    Validate smoking status
           IF INPUTS-ARE-VALID
               IF NOT SMOKING-STATUS-VALID
                   SET INPUTS-ARE-INVALID TO TRUE
                   MOVE 'INVALID SMOKING STATUS'
                       TO WS-ERROR-MSG
                   PERFORM 9100-LOG-ERROR
               END-IF
           END-IF

      *    Validate region
           IF INPUTS-ARE-VALID
               IF NOT REGION-IS-VALID
                   SET INPUTS-ARE-INVALID TO TRUE
                   MOVE 'INVALID REGION CODE' TO WS-ERROR-MSG
                   PERFORM 9100-LOG-ERROR
               END-IF
           END-IF
           .

Step 3: Calculate Premium Using EVALUATE

       3100-CALCULATE-PREMIUM.
      *    Step 3a: Determine base premium by plan type
           EVALUATE TRUE
               WHEN PLAN-BRONZE
                   MOVE 250.00 TO WS-BASE-PREMIUM
               WHEN PLAN-SILVER
                   MOVE 450.00 TO WS-BASE-PREMIUM
               WHEN PLAN-GOLD
                   MOVE 650.00 TO WS-BASE-PREMIUM
               WHEN PLAN-PLATINUM
                   MOVE 850.00 TO WS-BASE-PREMIUM
               WHEN OTHER
                   MOVE 0 TO WS-BASE-PREMIUM
                   PERFORM 9100-LOG-ERROR
           END-EVALUATE

      *    Step 3b: Apply age factor
           EVALUATE TRUE
               WHEN AGE-CHILD
                   MOVE 0.750 TO WS-AGE-FACTOR
               WHEN AGE-YOUNG-ADULT
                   MOVE 1.000 TO WS-AGE-FACTOR
               WHEN AGE-ADULT
                   MOVE 1.200 TO WS-AGE-FACTOR
               WHEN AGE-MATURE
                   MOVE 1.500 TO WS-AGE-FACTOR
               WHEN AGE-SENIOR
                   MOVE 1.800 TO WS-AGE-FACTOR
               WHEN OTHER
                   MOVE 1.000 TO WS-AGE-FACTOR
           END-EVALUATE

      *    Step 3c: Apply smoking surcharge
           IF IS-SMOKER
               MOVE 1.500 TO WS-SMOKING-FACTOR
           ELSE
               MOVE 1.000 TO WS-SMOKING-FACTOR
           END-IF

      *    Step 3d: Apply regional cost adjustment
           IF REGION-HIGH-COST
               MOVE 1.150 TO WS-REGION-FACTOR
           ELSE
               MOVE 1.000 TO WS-REGION-FACTOR
           END-IF

      *    Step 3e: Calculate final premium
           COMPUTE WS-FINAL-PREMIUM =
               WS-BASE-PREMIUM
               * WS-AGE-FACTOR
               * WS-SMOKING-FACTOR
               * WS-REGION-FACTOR
           .

Notice how each step uses the appropriate condition type: 88-level conditions for plan type and age range, simple condition names for boolean flags (IS-SMOKER), and group condition names for cost categories (REGION-HIGH-COST). The code reads like a specification document: "If the plan is Bronze, the base premium is $250. If the member is a senior, the age factor is 1.8. If they are in a high-cost region, apply a 15% regional adjustment."

Step 4: Business Rule Summary Using Combined Conditions

After calculation, we might need to apply special business rules that combine multiple conditions:

       3200-APPLY-SPECIAL-RULES.
      *    Rule 1: Seniors on premium plans get a 5% loyalty discount
           IF AGE-SENIOR AND PLAN-IS-PREMIUM
               COMPUTE WS-FINAL-PREMIUM =
                   WS-FINAL-PREMIUM * 0.95
           END-IF

      *    Rule 2: Young adult non-smokers get a wellness discount
           IF AGE-YOUNG-ADULT AND IS-NON-SMOKER
               COMPUTE WS-FINAL-PREMIUM =
                   WS-FINAL-PREMIUM * 0.97
           END-IF

      *    Rule 3: Cap premium at $3,000/month regardless
           IF WS-FINAL-PREMIUM > 3000.00
               MOVE 3000.00 TO WS-FINAL-PREMIUM
           END-IF
           .

This example shows the full lifecycle of conditional logic: validate inputs (class conditions + 88-levels), calculate using branching logic (EVALUATE TRUE), and apply business rules using combined conditions (AND/OR with 88-levels). Each step is in its own paragraph with a single responsibility. The data definitions with their 88-levels serve as the vocabulary of the business domain.

🧪 Try It Yourself: Premium Calculator

Build the complete premium calculator program in your Student Mainframe Lab. Add these extensions:

  1. Accept member data from the keyboard (ACCEPT statement)
  2. Display a formatted premium quote showing each factor
  3. Add a family plan option that multiplies the premium by the number of dependents
  4. Add an annual vs. monthly display option

6.9 GlobalBank Case Study: Transaction Routing Engine

Let us put everything together with a substantial real-world example. At GlobalBank, the transaction routing engine determines how to process each incoming transaction based on multiple factors: account type, transaction type, amount thresholds, customer tier, and regulatory flags.

Here is the DATA DIVISION setup:

       01  WS-ACCOUNT-TYPE          PIC X(01).
           88  ACCT-CHECKING         VALUE 'C'.
           88  ACCT-SAVINGS          VALUE 'S'.
           88  ACCT-MONEY-MARKET     VALUE 'M'.
           88  ACCT-CD               VALUE 'D'.
           88  ACCT-ALLOWS-WITHDRAWAL
                                     VALUE 'C' 'S' 'M'.
           88  ACCT-EARNS-INTEREST   VALUE 'S' 'M' 'D'.

       01  WS-TXN-TYPE              PIC X(02).
           88  TXN-WITHDRAWAL        VALUE 'WD'.
           88  TXN-DEPOSIT           VALUE 'DP'.
           88  TXN-TRANSFER          VALUE 'TF'.
           88  TXN-PAYMENT           VALUE 'PY'.
           88  TXN-INQUIRY           VALUE 'IQ'.
           88  TXN-CHANGES-BALANCE   VALUE 'WD' 'DP' 'TF' 'PY'.
           88  TXN-IS-VALID          VALUE 'WD' 'DP' 'TF'
                                           'PY' 'IQ'.

       01  WS-CUSTOMER-TIER         PIC X(01).
           88  CUST-PLATINUM         VALUE 'P'.
           88  CUST-GOLD             VALUE 'G'.
           88  CUST-SILVER           VALUE 'S'.
           88  CUST-STANDARD         VALUE 'R'.
           88  CUST-PREMIUM          VALUE 'P' 'G'.

       01  WS-TXN-AMOUNT            PIC S9(11)V99.
       01  WS-BALANCE               PIC S9(11)V99.
       01  WS-DAILY-WD-TOTAL        PIC S9(11)V99.
       01  WS-DAILY-WD-LIMIT        PIC S9(11)V99.

       01  WS-REGULATORY-FLAGS.
           05  WS-CTR-REQUIRED       PIC X(01).
               88  CTR-REQUIRED      VALUE 'Y'.
               88  CTR-NOT-REQUIRED  VALUE 'N'.
           05  WS-SAR-FLAG           PIC X(01).
               88  SAR-FLAGGED       VALUE 'Y'.
               88  SAR-NOT-FLAGGED   VALUE 'N'.
           05  WS-OFAC-FLAG          PIC X(01).
               88  OFAC-HIT          VALUE 'Y'.
               88  OFAC-CLEAR        VALUE 'N'.

And here is the routing logic in the PROCEDURE DIVISION:

       2000-ROUTE-TRANSACTION.
      *    -----------------------------------------------
      *    Step 1: Regulatory pre-checks (always first)
      *    -----------------------------------------------
           PERFORM 2010-CHECK-REGULATORY

           IF OFAC-HIT
               PERFORM 9200-BLOCK-TRANSACTION
               GO TO 2000-ROUTE-TXN-EXIT
           END-IF

      *    -----------------------------------------------
      *    Step 2: Validate basic transaction data
      *    -----------------------------------------------
           IF NOT TXN-IS-VALID
               MOVE 'INVALID TRANSACTION TYPE'
                   TO WS-ERROR-MSG
               PERFORM 9100-LOG-ERROR
               GO TO 2000-ROUTE-TXN-EXIT
           END-IF

      *    -----------------------------------------------
      *    Step 3: Route by transaction type
      *    -----------------------------------------------
           EVALUATE TRUE
               WHEN TXN-INQUIRY
                   PERFORM 3400-PROCESS-INQUIRY
               WHEN TXN-DEPOSIT
                   PERFORM 3200-PROCESS-DEPOSIT
               WHEN TXN-WITHDRAWAL
                   PERFORM 2100-VALIDATE-AND-WITHDRAW
               WHEN TXN-TRANSFER
                   PERFORM 2200-VALIDATE-AND-TRANSFER
               WHEN TXN-PAYMENT
                   PERFORM 2300-VALIDATE-AND-PAY
           END-EVALUATE

      *    -----------------------------------------------
      *    Step 4: Post-transaction regulatory reporting
      *    -----------------------------------------------
           IF TXN-CHANGES-BALANCE
               PERFORM 2400-CHECK-CTR-THRESHOLD
               IF SAR-FLAGGED
                   PERFORM 9300-FILE-SAR-REPORT
               END-IF
           END-IF
           .
       2000-ROUTE-TXN-EXIT.
           EXIT.

       2100-VALIDATE-AND-WITHDRAW.
           IF NOT ACCT-ALLOWS-WITHDRAWAL
               MOVE 'WITHDRAWAL NOT ALLOWED FOR THIS ACCT'
                   TO WS-ERROR-MSG
               PERFORM 9100-LOG-ERROR
           ELSE
               EVALUATE TRUE ALSO TRUE
                   WHEN WS-BALANCE < WS-TXN-AMOUNT
                       ALSO ANY
                       MOVE 'INSUFFICIENT FUNDS'
                           TO WS-ERROR-MSG
                       PERFORM 9100-LOG-ERROR
                   WHEN ANY
                       ALSO WS-DAILY-WD-TOTAL +
                            WS-TXN-AMOUNT >
                            WS-DAILY-WD-LIMIT
                       EVALUATE TRUE
                           WHEN CUST-PREMIUM
                               PERFORM 3150-PREMIUM-OVERRIDE
                           WHEN OTHER
                               MOVE 'DAILY LIMIT EXCEEDED'
                                   TO WS-ERROR-MSG
                               PERFORM 9100-LOG-ERROR
                       END-EVALUATE
                   WHEN OTHER
                       PERFORM 3100-PROCESS-WITHDRAWAL
               END-EVALUATE
           END-IF
           .

🔗 Cross-Reference: Notice how the regulatory checks (OFAC, CTR, SAR) are handled with condition names. We will see these regulatory patterns again in Chapter 29 when we cover compliance reporting, and in Chapter 35 when we discuss testing financial applications.

This code demonstrates several patterns from this chapter working together:

  1. Guard clauses for regulatory blocks (OFAC hit exits immediately)
  2. 88-level condition names for every status check (no raw value comparisons)
  3. Group condition names (TXN-CHANGES-BALANCE, ACCT-ALLOWS-WITHDRAWAL) for logical groupings
  4. EVALUATE TRUE for clean routing based on conditions
  5. EVALUATE TRUE ALSO TRUE for multi-dimensional validation
  6. Nested EVALUATE where appropriate (premium customer override within daily limit check)

6.9 MedClaim Case Study: Claim Adjudication Decision Tree

At MedClaim, claim adjudication is the most complex decision logic in the entire system. When a medical claim arrives, the adjudication engine must determine: Is the member eligible? Is the provider in-network? Does the service require prior authorization? What is the member's copay? What is the coinsurance? Is the deductible met? What is the plan maximum?

Sarah Kim spent three months documenting the adjudication decision tree, and it fills seventeen pages. Here is a simplified version that demonstrates the conditional logic patterns:

       01  WS-CLAIM-DECISION.
           05  WS-ADJ-STATUS         PIC X(02).
               88  ADJ-APPROVED      VALUE 'AP'.
               88  ADJ-DENIED        VALUE 'DN'.
               88  ADJ-PENDED        VALUE 'PD'.
               88  ADJ-REVIEW        VALUE 'RV'.
           05  WS-DENY-REASON        PIC X(04).
               88  DENY-INELIGIBLE   VALUE 'D001'.
               88  DENY-NOT-COVERED  VALUE 'D002'.
               88  DENY-NO-AUTH      VALUE 'D003'.
               88  DENY-MAX-REACHED  VALUE 'D004'.
               88  DENY-DUPLICATE    VALUE 'D005'.
           05  WS-PEND-REASON        PIC X(04).
               88  PEND-NEED-AUTH    VALUE 'P001'.
               88  PEND-NEED-RECORDS VALUE 'P002'.
               88  PEND-HIGH-DOLLAR  VALUE 'P003'.

       01  WS-MEMBER-INFO.
           05  WS-MEMBER-STATUS      PIC X(01).
               88  MEMBER-ACTIVE     VALUE 'A'.
               88  MEMBER-COBRA      VALUE 'C'.
               88  MEMBER-TERMED     VALUE 'T'.
               88  MEMBER-ELIGIBLE   VALUE 'A' 'C'.
           05  WS-PLAN-TYPE          PIC X(03).
               88  PLAN-HMO          VALUE 'HMO'.
               88  PLAN-PPO          VALUE 'PPO'.
               88  PLAN-POS          VALUE 'POS'.
               88  PLAN-REQUIRES-REFERRAL
                                     VALUE 'HMO' 'POS'.

       01  WS-SERVICE-INFO.
           05  WS-SERVICE-CODE       PIC X(05).
           05  WS-AUTH-REQUIRED      PIC X(01).
               88  AUTH-REQUIRED     VALUE 'Y'.
               88  AUTH-NOT-REQUIRED VALUE 'N'.
           05  WS-AUTH-ON-FILE       PIC X(01).
               88  AUTH-ON-FILE      VALUE 'Y'.
               88  AUTH-NOT-ON-FILE  VALUE 'N'.
           05  WS-PROVIDER-STATUS    PIC X(01).
               88  PROVIDER-IN-NET   VALUE 'I'.
               88  PROVIDER-OUT-NET  VALUE 'O'.
               88  PROVIDER-TERMED   VALUE 'T'.
               88  PROVIDER-VALID    VALUE 'I' 'O'.

The adjudication procedure:

       3000-ADJUDICATE-CLAIM.
      *    -----------------------------------------------
      *    Phase 1: Eligibility check
      *    -----------------------------------------------
           IF NOT MEMBER-ELIGIBLE
               SET ADJ-DENIED TO TRUE
               SET DENY-INELIGIBLE TO TRUE
               PERFORM 9500-LOG-DENIAL
               GO TO 3000-ADJUDICATE-EXIT
           END-IF

      *    -----------------------------------------------
      *    Phase 2: Duplicate check
      *    -----------------------------------------------
           PERFORM 3010-CHECK-DUPLICATE
           IF WS-IS-DUPLICATE = 'Y'
               SET ADJ-DENIED TO TRUE
               SET DENY-DUPLICATE TO TRUE
               PERFORM 9500-LOG-DENIAL
               GO TO 3000-ADJUDICATE-EXIT
           END-IF

      *    -----------------------------------------------
      *    Phase 3: Authorization check
      *    -----------------------------------------------
           EVALUATE TRUE
               WHEN AUTH-REQUIRED AND AUTH-ON-FILE
                   CONTINUE
               WHEN AUTH-REQUIRED AND AUTH-NOT-ON-FILE
                   EVALUATE TRUE
                       WHEN PLAN-HMO
                           SET ADJ-DENIED TO TRUE
                           SET DENY-NO-AUTH TO TRUE
                           PERFORM 9500-LOG-DENIAL
                           GO TO 3000-ADJUDICATE-EXIT
                       WHEN PLAN-PPO OR PLAN-POS
                           SET ADJ-PENDED TO TRUE
                           SET PEND-NEED-AUTH TO TRUE
                           GO TO 3000-ADJUDICATE-EXIT
                   END-EVALUATE
               WHEN AUTH-NOT-REQUIRED
                   CONTINUE
           END-EVALUATE

      *    -----------------------------------------------
      *    Phase 4: Network status and pricing
      *    -----------------------------------------------
           EVALUATE TRUE ALSO TRUE
               WHEN PROVIDER-IN-NET  ALSO PLAN-HMO
                   PERFORM 3100-PRICE-HMO-IN-NET
               WHEN PROVIDER-IN-NET  ALSO PLAN-PPO
                   PERFORM 3200-PRICE-PPO-IN-NET
               WHEN PROVIDER-IN-NET  ALSO PLAN-POS
                   PERFORM 3300-PRICE-POS-IN-NET
               WHEN PROVIDER-OUT-NET ALSO PLAN-HMO
                   SET ADJ-DENIED TO TRUE
                   SET DENY-NOT-COVERED TO TRUE
                   PERFORM 9500-LOG-DENIAL
                   GO TO 3000-ADJUDICATE-EXIT
               WHEN PROVIDER-OUT-NET ALSO PLAN-PPO
                   PERFORM 3400-PRICE-PPO-OUT-NET
               WHEN PROVIDER-OUT-NET ALSO PLAN-POS
                   PERFORM 3500-PRICE-POS-OUT-NET
               WHEN PROVIDER-TERMED  ALSO ANY
                   SET ADJ-PENDED TO TRUE
                   SET PEND-NEED-RECORDS TO TRUE
                   GO TO 3000-ADJUDICATE-EXIT
           END-EVALUATE

      *    -----------------------------------------------
      *    Phase 5: High-dollar review
      *    -----------------------------------------------
           IF WS-ALLOWED-AMOUNT > 10000
               SET ADJ-PENDED TO TRUE
               SET PEND-HIGH-DOLLAR TO TRUE
               GO TO 3000-ADJUDICATE-EXIT
           END-IF

           SET ADJ-APPROVED TO TRUE
           .
       3000-ADJUDICATE-EXIT.
           PERFORM 3900-WRITE-ADJUDICATION-RECORD
           .

📊 Pattern Analysis: This procedure demonstrates the "waterfall" pattern — a series of checks where each one can short-circuit the process. It processes in order of severity: eligibility (most critical, check first), duplicates, authorization, network/pricing, and finally dollar thresholds. Each phase either allows processing to continue or diverts to a terminal outcome (deny, pend) and exits.

🔵 Industry Context: This pattern — phased adjudication with early exit — is standard across the health insurance industry. Whether you work at a Blue Cross Blue Shield plan, a UnitedHealthcare subsidiary, or a small regional insurer, you will encounter this decision structure. The specific rules vary, but the architectural pattern is universal.


6.10 Common Pitfalls and Debugging Strategies

Pitfall 1: The Dangling ELSE

In deeply nested IF statements, it can be unclear which IF an ELSE belongs to:

      * DANGEROUS: Which IF does ELSE belong to?
       IF WS-STATUS = 'A'
           IF WS-BALANCE > 0
               PERFORM 3100-PROCESS
       ELSE
           PERFORM 9100-LOG-ERROR
       END-IF

In COBOL, ELSE binds to the nearest unmatched IF. So this ELSE belongs to the inner IF (WS-BALANCE > 0), not the outer IF. Using END-IF consistently eliminates this ambiguity:

      * CLEAR: END-IF makes structure explicit
       IF WS-STATUS = 'A'
           IF WS-BALANCE > 0
               PERFORM 3100-PROCESS
           ELSE
               PERFORM 9100-LOG-ERROR
           END-IF
       END-IF

Best Practice: Always use explicit END-IF terminators. Never rely on period-delimited conditionals in new code. Period-terminated IF statements were necessary before COBOL-85 introduced END-IF, but they are a maintenance hazard in modern programs.

Pitfall 2: Incorrect Implied Subject Scope

      * WRONG: Programmer intended to check both fields
       IF WS-STATE = 'NY' OR WS-COUNTRY = 'CA'
      * This actually means: IF WS-STATE = 'NY'
      *                       OR WS-STATE = 'CA'
      * (implied subject carries forward from WS-STATE)

Wait — is this actually what happens? Actually no. In this case, WS-COUNTRY = 'CA' is a separate relational condition, not an implied subject. The implied subject rule only applies when an identifier is omitted after OR or AND. The key is whether WS-COUNTRY is present as an explicit subject. Let us be precise:

      * Case 1: Explicit subjects — works as intended
       IF WS-STATE = 'NY' OR WS-COUNTRY = 'CA'
      * Checks: WS-STATE = 'NY' OR WS-COUNTRY = 'CA'

      * Case 2: Implied subject — might surprise you
       IF WS-STATE = 'NY' OR 'NJ'
      * Checks: WS-STATE = 'NY' OR WS-STATE = 'NJ'

      * Case 3: The real danger
       IF WS-STATE = 'NY' OR 'NJ' AND WS-TYPE = 'A'
      * Checks: WS-STATE = 'NY' OR
      *         (WS-STATE = 'NJ' AND WS-TYPE = 'A')
      * NOT: (WS-STATE = 'NY' OR 'NJ') AND WS-TYPE = 'A'

Case 3 is the real trap. The programmer might have intended "if the state is NY or NJ, and the type is A." But AND has higher precedence than OR, so the condition means "if the state is NY (regardless of type) OR if the state is NJ and the type is A." The fix:

       IF (WS-STATE = 'NY' OR 'NJ') AND WS-TYPE = 'A'

Pitfall 3: NUMERIC Test on Group Items

       01  WS-DATE-GROUP.
           05  WS-DATE-YEAR    PIC 9(04).
           05  WS-DATE-MONTH   PIC 9(02).
           05  WS-DATE-DAY     PIC 9(02).

      * WRONG: NUMERIC test on group item
       IF WS-DATE-GROUP IS NUMERIC

A group item is always treated as alphanumeric (PIC X) regardless of its subordinate items' pictures. The NUMERIC test on a group item tests whether all bytes are valid digit characters, which may not be what you intend if the group contains signed fields or embedded separators.

Pitfall 4: Forgetting WHEN OTHER

      * DANGEROUS: No WHEN OTHER
       EVALUATE WS-STATUS
           WHEN 'A' PERFORM 3100-ACTIVE
           WHEN 'C' PERFORM 3200-CLOSED
           WHEN 'S' PERFORM 3300-SUSPENDED
       END-EVALUATE

If WS-STATUS contains any value other than 'A', 'C', or 'S', the program silently does nothing. This is almost never the correct behavior. Always include WHEN OTHER, even if it just logs a warning.

Debugging Strategy: Condition Tracing

When debugging complex conditionals, temporarily add DISPLAY statements to trace which branch is taken:

       EVALUATE TRUE
           WHEN ACCT-IS-ACTIVE
               DISPLAY 'DEBUG: Branch ACTIVE taken'
               PERFORM 3100-PROCESS-ACTIVE
           WHEN ACCT-IS-SUSPENDED
               DISPLAY 'DEBUG: Branch SUSPENDED taken'
               PERFORM 3200-PROCESS-SUSPENDED
           WHEN OTHER
               DISPLAY 'DEBUG: Branch OTHER taken, '
                   'STATUS=' WS-ACCOUNT-STATUS
               PERFORM 9100-LOG-ERROR
       END-EVALUATE

⚠️ Warning: Always remove or comment out DISPLAY debugging before promoting code to production. Better yet, use a debugging flag:

       01  WS-DEBUG-MODE        PIC X(01) VALUE 'N'.
           88  DEBUG-ON         VALUE 'Y'.

      * In the logic:
       IF DEBUG-ON
           DISPLAY 'DEBUG: Branch ACTIVE taken'
       END-IF

6.11 Performance Considerations

In most COBOL programs, conditional logic is not a performance bottleneck. The compiler optimizes condition evaluation efficiently, and modern mainframe hardware evaluates conditions in nanoseconds. However, in high-volume batch programs processing millions of records, small optimizations can accumulate.

Order Your Conditions by Likelihood

In an IF-ELSE chain (or EVALUATE), put the most likely condition first:

      * If 80% of transactions are deposits, check that first
       EVALUATE TRUE
           WHEN TXN-DEPOSIT
               PERFORM 3200-PROCESS-DEPOSIT
           WHEN TXN-WITHDRAWAL
               PERFORM 3100-PROCESS-WITHDRAWAL
           WHEN TXN-INQUIRY
               PERFORM 3400-PROCESS-INQUIRY
           WHEN OTHER
               PERFORM 9100-LOG-ERROR
       END-EVALUATE

This minimizes the average number of condition evaluations. In a program processing 2.3 million transactions per day (GlobalBank's volume), putting the most common transaction type first versus last could save hundreds of thousands of unnecessary condition evaluations.

Short-Circuit Evaluation

COBOL does not guarantee short-circuit evaluation of AND/OR conditions. In languages like C or Java, if (x != null && x.getValue() > 0) is safe because the second condition is not evaluated if the first is false. COBOL may evaluate both conditions:

      * DANGEROUS in some compilers:
       IF WS-DIVISOR NOT = ZERO
           AND (WS-DIVIDEND / WS-DIVISOR) > WS-THRESHOLD

Some compilers may evaluate the division even when WS-DIVISOR is zero, causing an abend. The safe pattern:

       IF WS-DIVISOR NOT = ZERO
           IF (WS-DIVIDEND / WS-DIVISOR) > WS-THRESHOLD
               PERFORM 3100-ABOVE-THRESHOLD
           END-IF
       END-IF

📊 Compiler Note: IBM Enterprise COBOL can be configured for short-circuit evaluation with certain compiler options, but relying on this is non-portable. Nest your IFs for safety.


6.12 Conditional Logic in the Mainframe Ecosystem

Understanding conditional logic in COBOL is not just about knowing the syntax — it is about understanding how conditional logic interacts with the larger mainframe ecosystem.

Condition Evaluation and EBCDIC Collating Sequence

On IBM mainframes, character data is stored in EBCDIC (Extended Binary Coded Decimal Interchange Code), not ASCII. This affects character comparisons in unexpected ways:

EBCDIC Order ASCII Order
Spaces (lowest) Spaces (lowest)
Lowercase letters a-z Numbers 0-9
Uppercase letters A-Z Uppercase A-Z
Numbers 0-9 (highest) Lowercase a-z

This means that in EBCDIC, 'A' < 'a' and '9' > 'Z'. If you write:

       IF WS-CODE >= 'A' AND WS-CODE <= 'Z'

This works correctly in EBCDIC — it captures only uppercase letters. But in ASCII, this range would also include characters like [, \, ], ^, _, and the backtick. If your program runs on both mainframe (EBCDIC) and distributed (ASCII) platforms (using GnuCOBOL, for example), use class conditions instead:

       IF WS-CODE IS ALPHABETIC-UPPER

This is platform-independent because the class condition tests the character's category, not its numeric value.

⚠️ Portability Warning: The ALPHABETIC class condition considers the locale-specific definition of "alphabetic." On an EBCDIC system with a different code page (e.g., international EBCDIC with accented characters), the results may differ. Always test class conditions with your specific runtime environment.

Condition Evaluation and Packed Decimal

COBOL numeric fields can be stored in several formats: DISPLAY (one digit per byte), COMP-3 (packed decimal, two digits per byte plus sign), and COMP/BINARY. The IS NUMERIC class condition works correctly with all formats, but there is a subtle difference:

       01  WS-AMOUNT-DISPLAY PIC 9(05)    VALUE 12345.
       01  WS-AMOUNT-PACKED  PIC 9(05) COMP-3 VALUE 12345.

      * Both of these work correctly:
       IF WS-AMOUNT-DISPLAY IS NUMERIC ...   *> TRUE
       IF WS-AMOUNT-PACKED  IS NUMERIC ...   *> TRUE

However, if a COMP-3 field is corrupted (e.g., filled with X'FF' bytes from a file read error), the IS NUMERIC test will return false — which is exactly the behavior you want. This is why NUMERIC checks are essential for defensively validating data from external sources.

Performance of Conditional Evaluation

On modern IBM z/Architecture mainframes (z15 and z16), conditional evaluation is highly optimized:

  • Compare Immediate (CLI/CHI): Single conditions against literals compile to one or two machine instructions, executing in a single CPU cycle.
  • Branch on Condition (BC): IF/ELSE branches compile to conditional branch instructions with minimal overhead.
  • EVALUATE: The compiler may generate a branch table for EVALUATE with sequential literal values, achieving O(1) lookup rather than sequential comparison.

For most programs, conditional logic is not a performance concern. The exception is programs processing tens of millions of records per run, where even nanosecond-level differences accumulate. In those cases:

  1. Order EVALUATE WHEN clauses by frequency (most common first)
  2. Avoid complex computed conditions in tight inner loops
  3. Use condition names (88-levels) rather than complex OR chains — the compiler can optimize them more effectively
  4. If you must test multiple conditions, consider whether an EVALUATE with ALSO can replace multiple sequential IF statements

📊 Benchmark Data: At GlobalBank, Priya Kapoor measured the performance difference between a nested IF chain (6 levels deep) and an equivalent EVALUATE TRUE with 6 WHEN clauses. Over 10 million iterations, the EVALUATE version was 3.2% faster — a meaningful difference in a tight inner loop, though negligible in most programs.

Conditional Logic and Copybooks

In large systems, condition names (88-levels) should be defined in copybooks (shared data definitions) rather than in individual programs. This ensures that every program uses the same condition names and values:

      * COPYBOOK: ACCT-STATUS-DEF.cpy
       01  WS-ACCOUNT-STATUS        PIC X(01).
           88  ACCT-IS-ACTIVE        VALUE 'A'.
           88  ACCT-IS-CLOSED        VALUE 'C'.
           88  ACCT-IS-SUSPENDED     VALUE 'S'.
           88  ACCT-IS-PENDING       VALUE 'P'.
           88  ACCT-IS-RESTRICTED    VALUE 'R'.
           88  ACCT-IS-PROCESSABLE   VALUE 'A' 'P'.
           88  ACCT-STATUS-VALID     VALUE 'A' 'C' 'S' 'P' 'R'.

When a new status code is added, the copybook is updated once, and all programs that COPY it automatically get the updated definitions. We will explore copybooks in depth in Chapter 9.

🔗 Cross-Reference: The copybook approach to 88-level definitions is the single most effective technique for preventing the "missing status code" bugs described in the MedClaim case study (Section 6.11). When the 88-level is defined in a copybook and all programs COPY it, adding a new value means updating one file rather than hunting through dozens of programs.


6.13 Putting It All Together: Style Guide for Conditional Logic

Based on everything in this chapter, here is a style guide for writing maintainable conditional logic in COBOL:

  1. Define 88-level condition names for every status/type/flag field. This is non-negotiable in professional COBOL. Raw value comparisons (= 'A', = 'Y') should be rare in your PROCEDURE DIVISION.

  2. Use EVALUATE TRUE instead of deeply nested IF. If you have more than three conditions to check, EVALUATE TRUE is almost always more readable.

  3. Use EVALUATE with ALSO for multi-dimensional decisions. When the action depends on two or more independent factors, a multi-subject EVALUATE is a decision table in code.

  4. Always include WHEN OTHER. No exceptions.

  5. Always use END-IF and END-EVALUATE. Never use period-delimited conditionals in new code.

  6. Use parentheses in all complex conditions. Even when precedence would give the correct result, parentheses communicate intent.

  7. Limit nesting to three levels. If you need deeper nesting, refactor into paragraphs or use EVALUATE.

  8. Create group condition names. If you frequently check "is this status one of several values," create an 88-level that groups those values.

  9. Use SET rather than MOVE to set condition names. SET ACCT-IS-ACTIVE TO TRUE is self-documenting; MOVE 'A' TO WS-ACCT-STATUS requires knowledge of the encoding.

  10. Validate before you calculate. Always use IS NUMERIC before using alphanumeric input in arithmetic.

🔗 Looking Ahead: In Chapter 7, we will explore iteration patterns — PERFORM in all its forms. You will see how the conditional patterns from this chapter combine with loop constructs to build the batch processing engines that power enterprise systems. In Chapter 8, we will zoom out to program design: how paragraphs and sections organize your conditional and iterative logic into maintainable, readable programs.


6.14 Working Through a Complex Business Rule

To solidify everything in this chapter, let us trace through a complete business rule implementation from specification to code. This is the process that professional COBOL programmers follow when translating business requirements into conditional logic.

The Business Rule

GlobalBank's fee schedule for wire transfers is as follows:

Wire Transfer Fee Schedule (effective 2024-01-01): 1. Domestic wires (US only): - Standard (next business day): $15 for balances under $10,000; $10 for balances $10,000-$99,999; FREE for balances $100,000+ - Same-day: $25 for all account tiers 2. International wires: - Standard: $45 regardless of balance - Same-day: $65 regardless of balance 3. Fee waivers: - Platinum customers: all domestic wire fees waived - Gold customers: standard domestic wire fees waived (same-day still applies) 4. Monthly limit: Maximum 20 wire transfers per month per account (all types combined). Attempts beyond 20 must be rejected.

Step 1: Define the Data

       01  WS-WIRE-REQUEST.
           05  WS-WIRE-TYPE         PIC X(01).
               88  WIRE-DOMESTIC     VALUE 'D'.
               88  WIRE-INTERNATIONAL VALUE 'I'.
               88  WIRE-TYPE-VALID   VALUE 'D' 'I'.
           05  WS-WIRE-SPEED        PIC X(01).
               88  WIRE-STANDARD     VALUE 'S'.
               88  WIRE-SAME-DAY     VALUE 'E'.
               88  WIRE-SPEED-VALID  VALUE 'S' 'E'.
           05  WS-WIRE-AMOUNT       PIC S9(11)V99.

       01  WS-ACCT-BALANCE          PIC S9(11)V99.
           88  BAL-UNDER-10K        VALUE -999999999.99
                                    THRU 9999.99.
           88  BAL-10K-TO-100K      VALUE 10000.00
                                    THRU 99999.99.
           88  BAL-100K-PLUS        VALUE 100000.00
                                    THRU 999999999.99.

       01  WS-CUST-TIER             PIC X(01).
           88  CUST-PLATINUM         VALUE 'P'.
           88  CUST-GOLD             VALUE 'G'.
           88  CUST-SILVER           VALUE 'S'.
           88  CUST-STANDARD         VALUE 'R'.
           88  CUST-IS-PREMIUM       VALUE 'P' 'G'.

       01  WS-MONTHLY-WIRE-COUNT    PIC 9(03).
       01  WS-WIRE-MONTHLY-LIMIT    PIC 9(03) VALUE 20.
       01  WS-WIRE-FEE              PIC S9(05)V99.
       01  WS-WIRE-RESULT           PIC X(01).
           88  WIRE-APPROVED         VALUE 'A'.
           88  WIRE-REJECTED         VALUE 'R'.

Step 2: Validate Inputs

       4000-PROCESS-WIRE-REQUEST.
      *    Validate inputs first
           IF NOT WIRE-TYPE-VALID
               MOVE 'INVALID WIRE TYPE' TO WS-ERROR-MSG
               SET WIRE-REJECTED TO TRUE
               PERFORM 9100-LOG-ERROR
               GO TO 4000-WIRE-EXIT
           END-IF

           IF NOT WIRE-SPEED-VALID
               MOVE 'INVALID WIRE SPEED' TO WS-ERROR-MSG
               SET WIRE-REJECTED TO TRUE
               PERFORM 9100-LOG-ERROR
               GO TO 4000-WIRE-EXIT
           END-IF

           IF WS-WIRE-AMOUNT IS NOT POSITIVE
               MOVE 'WIRE AMOUNT MUST BE POSITIVE'
                   TO WS-ERROR-MSG
               SET WIRE-REJECTED TO TRUE
               PERFORM 9100-LOG-ERROR
               GO TO 4000-WIRE-EXIT
           END-IF

      *    Check monthly limit
           IF WS-MONTHLY-WIRE-COUNT >= WS-WIRE-MONTHLY-LIMIT
               MOVE 'MONTHLY WIRE LIMIT EXCEEDED'
                   TO WS-ERROR-MSG
               SET WIRE-REJECTED TO TRUE
               PERFORM 9100-LOG-ERROR
               GO TO 4000-WIRE-EXIT
           END-IF

      *    Calculate fee
           PERFORM 4100-DETERMINE-WIRE-FEE

      *    Apply fee waivers
           PERFORM 4200-APPLY-FEE-WAIVERS

           SET WIRE-APPROVED TO TRUE
           ADD 1 TO WS-MONTHLY-WIRE-COUNT
           .
       4000-WIRE-EXIT.
           EXIT.

Step 3: Determine Base Fee

       4100-DETERMINE-WIRE-FEE.
           EVALUATE TRUE ALSO TRUE
               WHEN WIRE-DOMESTIC  ALSO WIRE-STANDARD
                   EVALUATE TRUE
                       WHEN BAL-100K-PLUS
                           MOVE 0 TO WS-WIRE-FEE
                       WHEN BAL-10K-TO-100K
                           MOVE 10.00 TO WS-WIRE-FEE
                       WHEN OTHER
                           MOVE 15.00 TO WS-WIRE-FEE
                   END-EVALUATE
               WHEN WIRE-DOMESTIC  ALSO WIRE-SAME-DAY
                   MOVE 25.00 TO WS-WIRE-FEE
               WHEN WIRE-INTERNATIONAL ALSO WIRE-STANDARD
                   MOVE 45.00 TO WS-WIRE-FEE
               WHEN WIRE-INTERNATIONAL ALSO WIRE-SAME-DAY
                   MOVE 65.00 TO WS-WIRE-FEE
               WHEN OTHER
                   MOVE 0 TO WS-WIRE-FEE
                   MOVE 'UNEXPECTED WIRE COMBINATION'
                       TO WS-ERROR-MSG
                   PERFORM 9100-LOG-ERROR
           END-EVALUATE
           .

Step 4: Apply Fee Waivers

       4200-APPLY-FEE-WAIVERS.
      *    Platinum: all domestic fees waived
           IF CUST-PLATINUM AND WIRE-DOMESTIC
               MOVE 0 TO WS-WIRE-FEE
           END-IF

      *    Gold: standard domestic fees waived
           IF CUST-GOLD AND WIRE-DOMESTIC AND WIRE-STANDARD
               MOVE 0 TO WS-WIRE-FEE
           END-IF
           .

Analysis

Let us count the condition types used in this solution:

Condition Type Usage
88-level condition names 18 defined, used throughout
EVALUATE TRUE 2 instances (fee determination, balance tier)
EVALUATE TRUE ALSO TRUE 1 instance (wire type + speed)
Class conditions 1 (IS POSITIVE check on amount)
Complex conditions 3 (AND combinations for fee waivers)
Guard clauses 4 (input validation with GO TO exit)
Group condition names 2 (CUST-IS-PREMIUM, WIRE-TYPE-VALID)
WHEN OTHER In every EVALUATE

Every technique from this chapter is represented. The code is readable — Sarah Kim verified it against the fee schedule document in ten minutes. It is maintainable — adding a new wire speed or a new customer tier requires modifying one or two clearly identified locations. And it is defensive — invalid inputs are caught and rejected before any fee calculation occurs.

🧪 Try It Yourself: Extending the Wire Fee Calculator

Extend the wire fee calculator with these additional requirements:

  1. Add a "recurring wire" flag (monthly automatic wires). Recurring wires get a 25% fee discount.
  2. Add a currency conversion surcharge for international wires: 1% of the wire amount, minimum $10, maximum $50.
  3. Add a fraud check: wires over $50,000 to new recipients (first wire to that destination in the last 90 days) must be flagged for manual review (pended, not rejected).
  4. Display a formatted fee summary showing the base fee, each adjustment, and the final fee.

Chapter Summary

This chapter has taken you from basic IF statements to the full power of COBOL's conditional logic toolkit. You have learned to manage nested IF complexity through guard clauses, condition flags, and paragraph extraction. You have explored EVALUATE in its many forms — basic, TRUE, ALSO, and with ranges. You have mastered 88-level condition names, the single most important readability tool in the COBOL programmer's arsenal. And you have learned to combine conditions safely using AND, OR, NOT, and parentheses.

The central lesson is this: readability is not opposed to correctness — it enables it. Code that is easy to read is easy to verify, easy to test, easy to debug, and easy to modify. In the world of enterprise COBOL, where programs process billions of dollars and affect millions of people, readable conditional logic is not a stylistic preference. It is a professional obligation.

As Maria Chen tells every new developer on her team: "Write your conditions so that a business analyst can read them. If Sarah Kim can look at your EVALUATE and understand what it does, you have written good code. If she cannot, rewrite it until she can."


Key Terms

Term Definition
Condition name An 88-level item that associates a meaningful name with one or more values of a data item
EVALUATE A multi-way branch statement that acts as a decision table
Guard clause A conditional check at the beginning of a procedure that exits early for invalid conditions
Implied subject A COBOL shorthand where the subject of a condition is carried forward from a previous condition in the same statement
Class condition A test of whether a data item's contents belong to a category (NUMERIC, ALPHABETIC, etc.)
Sign condition A test of whether a numeric value is POSITIVE, NEGATIVE, or ZERO
Complex condition A condition formed by combining simple conditions with AND, OR, and NOT
Short-circuit evaluation Optimization where the second operand of AND/OR is not evaluated if the result is determined by the first — not guaranteed in COBOL
Decision table A tabular representation of multi-dimensional conditional logic, implementable via EVALUATE with ALSO
WHEN OTHER The catch-all branch in an EVALUATE statement, analogous to a default case