23 min read

> "In COBOL, a penny is a penny. In floating point, a penny is 0.009999999776482582..." — anonymous mainframe programmer

Chapter 5: Numeric Precision and Arithmetic

"In COBOL, a penny is a penny. In floating point, a penny is 0.009999999776482582..." — anonymous mainframe programmer

Financial computing demands something that most programming languages take for granted: exact decimal arithmetic. When a bank calculates interest on 2.3 million accounts, when an insurance company prorates a $47,832.16 claim across four benefit categories, when a payroll system computes tax withholdings for ten thousand employees — every cent must be accounted for. Not approximately. Exactly.

This is the domain where COBOL excels. In your first COBOL course, you learned the basics of ADD, SUBTRACT, MULTIPLY, DIVIDE, and COMPUTE. In this chapter, we go deep into the mechanics of how COBOL performs arithmetic: how the compiler manages intermediate result precision, why packed decimal (COMP-3) avoids the floating-point trap, how rounding works, and what happens when a result is too large for its target field. We will build real financial calculation programs — interest computation, loan amortization, claim proration — and learn the defensive programming patterns that experienced COBOL developers use to ensure correctness.

At GlobalBank, Maria Chen has a saying that Derek Washington hears on his first day working with the BAL-CALC program: "If you cannot explain where every fraction of a penny went, your code is not ready for production." By the end of this chapter, you will be able to meet that standard.


5.1 The Two Families of COBOL Arithmetic

COBOL provides two distinct approaches to arithmetic:

  1. Explicit arithmetic verbs: ADD, SUBTRACT, MULTIPLY, DIVIDE — individual statements for each operation
  2. The COMPUTE statement: An algebraic expression evaluator that handles complex formulas in a single statement

Both produce identical results when used correctly. The choice between them is a matter of readability, complexity, and shop standards.

5.1.1 When to Use Explicit Verbs

Explicit verbs are clearest when the operation is simple and the business meaning is obvious:

           ADD WS-DEPOSIT-AMT TO ACCT-BALANCE
           SUBTRACT WS-FEE TO ACCT-BALANCE
           MULTIPLY WS-QUANTITY BY WS-UNIT-PRICE
               GIVING WS-LINE-TOTAL
           DIVIDE WS-TOTAL-AMOUNT BY WS-NUM-PAYMENTS
               GIVING WS-PAYMENT-AMT REMAINDER WS-LEFTOVER

Each statement reads almost like an English sentence. For simple business operations — adding a deposit, subtracting a fee, computing a line total — explicit verbs are self-documenting.

5.1.2 When to Use COMPUTE

COMPUTE is essential when the formula is complex or involves multiple operations:

           COMPUTE WS-MONTHLY-PAYMENT =
               (WS-PRINCIPAL * WS-MONTHLY-RATE *
                (1 + WS-MONTHLY-RATE) ** WS-NUM-PAYMENTS) /
               ((1 + WS-MONTHLY-RATE) ** WS-NUM-PAYMENTS - 1)

Trying to express this loan payment formula with explicit verbs would require multiple intermediate variables and many more lines of code, actually reducing readability.

💡 Key Insight: The choice is not "one or the other." Good COBOL programs use both. At MedClaim, James Okafor's coding standard says: "Use ADD/SUBTRACT for simple accumulations. Use COMPUTE for formulas with more than two operands or any exponentiation."


5.2 The Explicit Arithmetic Verbs in Depth

5.2.1 ADD

The ADD verb has several forms:

      *--- Form 1: ADD ... TO (modifies the target) ---
           ADD WS-AMOUNT TO WS-TOTAL
      *>   WS-TOTAL = WS-TOTAL + WS-AMOUNT

      *--- Form 2: ADD ... GIVING (stores in a new target) ---
           ADD WS-AMT-1 WS-AMT-2 WS-AMT-3
               GIVING WS-SUM
      *>   WS-SUM = WS-AMT-1 + WS-AMT-2 + WS-AMT-3

      *--- Form 3: ADD CORRESPONDING ---
           ADD CORR WS-MONTHLY-AMOUNTS TO WS-YEARLY-TOTALS
      *>   Adds each identically-named subordinate field

ADD CORRESPONDING (abbreviated ADD CORR) is a powerful shortcut when two group items have identically named subordinate fields:

       01  WS-MONTHLY-AMOUNTS.
           05  WS-PRINCIPAL    PIC S9(9)V99 COMP-3.
           05  WS-INTEREST     PIC S9(9)V99 COMP-3.
           05  WS-FEES         PIC S9(7)V99 COMP-3.

       01  WS-YEARLY-TOTALS.
           05  WS-PRINCIPAL    PIC S9(11)V99 COMP-3.
           05  WS-INTEREST     PIC S9(11)V99 COMP-3.
           05  WS-FEES         PIC S9(9)V99 COMP-3.
           05  WS-TAX          PIC S9(9)V99 COMP-3.

           ADD CORR WS-MONTHLY-AMOUNTS TO WS-YEARLY-TOTALS
      *>   Adds WS-PRINCIPAL, WS-INTEREST, and WS-FEES
      *>   (matching names). WS-TAX is NOT affected
      *>   (no match in source).

⚠️ Warning: ADD CORR matches by name, not by position. If field names do not match exactly (including hyphens), they are skipped. This can lead to subtle bugs if someone renames a field in one group but not the other.

5.2.2 SUBTRACT

      *--- Form 1: SUBTRACT ... FROM ---
           SUBTRACT WS-FEE FROM ACCT-BALANCE
      *>   ACCT-BALANCE = ACCT-BALANCE - WS-FEE

      *--- Form 2: SUBTRACT ... FROM ... GIVING ---
           SUBTRACT WS-DEDUCTIONS FROM WS-GROSS-PAY
               GIVING WS-NET-PAY
      *>   WS-NET-PAY = WS-GROSS-PAY - WS-DEDUCTIONS
      *>   (WS-GROSS-PAY is NOT modified)

      *--- Form 3: SUBTRACT CORRESPONDING ---
           SUBTRACT CORR WS-ADJUSTMENTS FROM WS-TOTALS

💡 Important Distinction: SUBTRACT A FROM B modifies B. SUBTRACT A FROM B GIVING C does NOT modify B — it stores the result in C. This distinction trips up beginners regularly.

5.2.3 MULTIPLY

      *--- Form 1: MULTIPLY ... BY (modifies the target) ---
           MULTIPLY WS-QUANTITY BY WS-UNIT-PRICE
      *>   WS-UNIT-PRICE = WS-UNIT-PRICE * WS-QUANTITY
      *>   NOTE: The second operand (BY) is modified!

      *--- Form 2: MULTIPLY ... BY ... GIVING ---
           MULTIPLY WS-QUANTITY BY WS-UNIT-PRICE
               GIVING WS-LINE-TOTAL
      *>   WS-LINE-TOTAL = WS-QUANTITY * WS-UNIT-PRICE
      *>   Neither WS-QUANTITY nor WS-UNIT-PRICE is modified

⚠️ Watch Out: In Form 1, it is the second operand (after BY) that gets modified, not the first. This is counterintuitive. Most experienced developers use the GIVING form to avoid confusion.

5.2.4 DIVIDE

DIVIDE is the most feature-rich arithmetic verb:

      *--- Form 1: DIVIDE ... INTO ---
           DIVIDE WS-DIVISOR INTO WS-DIVIDEND
      *>   WS-DIVIDEND = WS-DIVIDEND / WS-DIVISOR

      *--- Form 2: DIVIDE ... INTO ... GIVING ---
           DIVIDE WS-COUNT INTO WS-TOTAL
               GIVING WS-AVERAGE
      *>   WS-AVERAGE = WS-TOTAL / WS-COUNT

      *--- Form 3: DIVIDE ... BY ... GIVING ---
           DIVIDE WS-TOTAL BY WS-COUNT
               GIVING WS-AVERAGE
      *>   WS-AVERAGE = WS-TOTAL / WS-COUNT
      *>   (Same result as Form 2 but reads more naturally)

      *--- Form 4: With REMAINDER ---
           DIVIDE WS-TOTAL-CENTS BY WS-NUM-ACCOUNTS
               GIVING WS-PER-ACCOUNT REMAINDER WS-LEFTOVER
      *>   Integer division with remainder — essential for
      *>   penny-exact distribution (see Section 5.10)

The REMAINDER Clause

REMAINDER is uniquely important for financial programming. When distributing a total across multiple recipients, integer division often leaves a remainder:

       77  WS-TOTAL-AMT       PIC S9(7)V99 COMP-3 VALUE 1000.00.
       77  WS-NUM-SHARES       PIC 9(3)     COMP   VALUE 3.
       77  WS-PER-SHARE        PIC S9(7)V99 COMP-3.
       77  WS-REMAINDER        PIC S9(7)V99 COMP-3.

           DIVIDE WS-TOTAL-AMT BY WS-NUM-SHARES
               GIVING WS-PER-SHARE REMAINDER WS-REMAINDER
      *>   WS-PER-SHARE = 333.33
      *>   WS-REMAINDER = 0.01
      *>   333.33 * 3 = 999.99 — we need to account for
      *>   the missing penny!

We will return to this critical pattern in Section 5.10 (Financial Calculation Patterns).


5.3 The COMPUTE Statement in Depth

COMPUTE evaluates an algebraic expression and stores the result:

           COMPUTE target = expression
               [ROUNDED]
               [ON SIZE ERROR imperative-statement]
               [NOT ON SIZE ERROR imperative-statement]
           END-COMPUTE

5.3.1 Operators and Precedence

COMPUTE supports these operators, in order of precedence (highest first):

Precedence Operator Meaning
1 (highest) ** Exponentiation
2 * / Multiplication, Division
3 (lowest) + - Addition, Subtraction

Parentheses override precedence, just as in standard algebra.

      *--- Precedence example ---
           COMPUTE WS-RESULT = 2 + 3 * 4
      *>   WS-RESULT = 14 (not 20)
      *>   Multiplication before addition

           COMPUTE WS-RESULT = (2 + 3) * 4
      *>   WS-RESULT = 20
      *>   Parentheses force addition first

5.3.2 Exponentiation

The ** operator raises a number to a power:

           COMPUTE WS-COMPOUND = WS-PRINCIPAL *
               (1 + WS-RATE) ** WS-PERIODS
      *>   Compound interest formula: P * (1 + r)^n

⚠️ Precision Warning: Exponentiation with non-integer exponents forces the compiler to use floating-point internally, even if the operands are COMP-3. This can introduce tiny rounding errors. For financial calculations involving exponentiation, always use ROUNDED and verify results against expected values.

5.3.3 Multiple Targets

COMPUTE can store the result in multiple targets simultaneously:

           COMPUTE WS-SAVE-VALUE
                   WS-DISPLAY-VALUE
                   WS-REPORT-VALUE  =
               WS-GROSS - WS-TAX - WS-DEDUCTIONS

Each target can have its own ROUNDED phrase (COBOL 2002+):

           COMPUTE WS-PRECISE-VALUE
                   WS-ROUNDED-VALUE ROUNDED  =
               WS-AMOUNT / WS-DIVISOR

5.4 The ROUNDED Phrase

When an arithmetic result has more decimal places than the target field can hold, COBOL must either truncate or round. By default, COBOL truncates (chops off the excess digits). The ROUNDED phrase requests rounding instead.

5.4.1 Default Behavior: Truncation

       77  WS-RESULT          PIC S9(5)V99 COMP-3.

           COMPUTE WS-RESULT = 100.00 / 3
      *>   True result: 33.333333...
      *>   WS-RESULT = 33.33 (truncated — excess digits dropped)

5.4.2 ROUNDED Behavior

           COMPUTE WS-RESULT ROUNDED = 100.00 / 3
      *>   True result: 33.333333...
      *>   WS-RESULT = 33.33 (rounded — .333 rounds down)

           COMPUTE WS-RESULT ROUNDED = 200.00 / 3
      *>   True result: 66.666666...
      *>   WS-RESULT = 66.67 (rounded — .666 rounds up)

ROUNDED can be applied to any arithmetic statement:

           ADD WS-A TO WS-B ROUNDED
           SUBTRACT WS-X FROM WS-Y GIVING WS-Z ROUNDED
           MULTIPLY WS-P BY WS-Q GIVING WS-R ROUNDED
           DIVIDE WS-M BY WS-N GIVING WS-O ROUNDED

5.4.3 How COBOL Rounds: The Rules

In standard COBOL (through COBOL 85), rounding follows the "round half up" rule:

  • If the first excess digit is 0-4: round down (truncate)
  • If the first excess digit is 5-9: round up (add 1 to the last retained digit)
Value 33.334 → PIC V99 ROUNDED → 33.33  (4 < 5, round down)
Value 33.335 → PIC V99 ROUNDED → 33.34  (5 >= 5, round up)
Value 33.336 → PIC V99 ROUNDED → 33.34  (6 >= 5, round up)
Value -33.335 → PIC SV99 ROUNDED → -33.34  (rounds away from zero)

5.4.4 NEAREST-EVEN Rounding (COBOL 2002+)

COBOL 2002 introduced the ROUNDED MODE clause with additional rounding modes:

           COMPUTE WS-RESULT
               ROUNDED MODE IS NEAREST-EVEN
               = WS-AMOUNT / WS-DIVISOR

Available modes (compiler support varies):

Mode Behavior
NEAREST-AWAY-FROM-ZERO Traditional COBOL rounding (default)
NEAREST-EVEN Banker's rounding — .5 rounds to the nearest even digit
NEAREST-TOWARD-ZERO .5 rounds toward zero
TRUNCATION Same as no ROUNDED
TOWARD-GREATER Always round toward positive infinity
TOWARD-LESSER Always round toward negative infinity

Banker's Rounding (NEAREST-EVEN) is increasingly required by financial regulations because it eliminates the statistical bias of always rounding .5 up:

Value 33.335 → NEAREST-EVEN → 33.34  (rounds to even digit 4)
Value 33.345 → NEAREST-EVEN → 33.34  (rounds to even digit 4)
Value 33.355 → NEAREST-EVEN → 33.36  (rounds to even digit 6)
Value 33.365 → NEAREST-EVEN → 33.36  (rounds to even digit 6)

Over millions of transactions, NEAREST-EVEN produces a more balanced distribution than always rounding .5 up.

📊 Mathematical Analysis: Rounding Bias

Consider rounding 10,000 random values with two decimal places to one decimal place. The digit in the second decimal position is equally likely to be 0-9:

  • Digits 0-4 round down (5 out of 10)
  • Digits 5-9 round up with "round half up" (5 out of 10)

This seems balanced, but digits 1-4 all round down while 6-9 all round up, and 0 and 5 are the tiebreakers. Since 0 always truncates (no change) and 5 always rounds up, there is a slight upward bias. Over millions of financial transactions, this bias accumulates. Banker's rounding resolves this by making the .5 case go up half the time and down half the time, depending on whether the preceding digit is odd or even.


5.5 ON SIZE ERROR: Overflow Handling

When an arithmetic result is too large for the target field, a size error occurs. Without ON SIZE ERROR, the result is silently truncated — the high-order digits are lost, and your program continues with a wrong value.

5.5.1 The Silent Truncation Problem

       77  WS-SMALL-FIELD     PIC 9(3)V99 COMP-3.

           COMPUTE WS-SMALL-FIELD = 500.00 * 300.00
      *>   True result: 150000.00
      *>   WS-SMALL-FIELD can only hold 999.99
      *>   WITHOUT SIZE ERROR: silently stores 000.00 (truncated!)
      *>   Your program continues with a WRONG value.

This is one of the most dangerous behaviors in COBOL. A $150,000 transaction becomes $0.00 with no error message, no abend, no indication that anything went wrong.

5.5.2 Defensive Arithmetic with ON SIZE ERROR

           COMPUTE WS-SMALL-FIELD = 500.00 * 300.00
               ON SIZE ERROR
                   DISPLAY 'OVERFLOW: Result exceeds field size'
                   MOVE ZERO TO WS-SMALL-FIELD
                   SET WS-CALC-ERROR TO TRUE
               NOT ON SIZE ERROR
                   SET WS-CALC-OK TO TRUE
           END-COMPUTE

When a size error occurs: 1. The target field is not modified (retains its previous value) 2. The ON SIZE ERROR imperative statement executes 3. The NOT ON SIZE ERROR statement does NOT execute

When no size error occurs: 1. The target field receives the computed value 2. The NOT ON SIZE ERROR statement executes (if present) 3. The ON SIZE ERROR statement does NOT execute

5.5.3 SIZE ERROR with Division by Zero

Division by zero is treated as a size error:

           DIVIDE WS-TOTAL BY WS-COUNT
               GIVING WS-AVERAGE
               ON SIZE ERROR
                   DISPLAY 'ERROR: Division by zero or overflow'
                   MOVE ZERO TO WS-AVERAGE
           END-DIVIDE

⚠️ Critical Practice: Every DIVIDE statement in production code should have ON SIZE ERROR. Division by zero is not always obvious — the divisor might come from a file record, a database query, or user input. James Okafor's rule at MedClaim: "Every DIVIDE gets ON SIZE ERROR. No exceptions. I don't care if you 'know' the divisor can never be zero — prove it to the compiler."

5.5.4 SIZE ERROR Scope

ON SIZE ERROR applies to the entire arithmetic statement, including intermediate results. If any step in a COMPUTE expression overflows, the size error is triggered:

           COMPUTE WS-RESULT =
               WS-BIG-NUM-1 * WS-BIG-NUM-2 / WS-DIVISOR
               ON SIZE ERROR
                   PERFORM HANDLE-OVERFLOW
           END-COMPUTE
      *>   If WS-BIG-NUM-1 * WS-BIG-NUM-2 overflows the
      *>   intermediate result field, SIZE ERROR triggers
      *>   even if the final result would have fit.

This brings us to one of the most important (and most confusing) topics in COBOL arithmetic: intermediate result precision.


5.6 Intermediate Result Precision

When COBOL evaluates a complex expression, it must store partial results in temporary fields. The size and precision of these intermediate fields are determined by the compiler according to specific rules — and understanding these rules is essential for avoiding truncation in complex calculations.

5.6.1 The Problem

Consider this calculation:

       77  WS-PRINCIPAL       PIC S9(9)V99 COMP-3.
       77  WS-RATE            PIC SV9(6)   COMP-3.
       77  WS-DAYS            PIC 9(3)     COMP.
       77  WS-INTEREST        PIC S9(7)V99 COMP-3.

           COMPUTE WS-INTEREST =
               WS-PRINCIPAL * WS-RATE * WS-DAYS / 365

The compiler must evaluate this step by step: 1. Compute WS-PRINCIPAL * WS-RATE → intermediate result 1 2. Compute intermediate-1 * WS-DAYS → intermediate result 2 3. Compute intermediate-2 / 365 → final result

What precision do intermediates 1 and 2 have? If they are too small, significant digits are lost before the final result is computed.

5.6.2 IBM Enterprise COBOL Rules

For IBM Enterprise COBOL (the most common mainframe compiler), intermediate result precision follows these guidelines:

For multiplication: The intermediate result has enough digits to hold the full product without loss. - Integer digits = sum of integer digits of the operands - Decimal digits = sum of decimal digits of the operands

For division: The intermediate result has enough decimal digits to fill the final target.

For complex expressions: The compiler analyzes the entire expression to determine the maximum precision needed.

Let us trace our example:

WS-PRINCIPAL: 9 integer, 2 decimal = 11 digits
WS-RATE:      0 integer, 6 decimal = 6 digits
WS-DAYS:      3 integer, 0 decimal = 3 digits

Step 1: PRINCIPAL * RATE
  Integer digits: 9 + 0 = 9
  Decimal digits: 2 + 6 = 8
  Intermediate: 17 digits (9 integer + 8 decimal)

Step 2: (Step 1) * DAYS
  Integer digits: 9 + 3 = 12
  Decimal digits: 8 + 0 = 8
  Intermediate: 20 digits (12 integer + 8 decimal)

Step 3: (Step 2) / 365
  Final target: WS-INTEREST has 2 decimal places
  Intermediate keeps enough precision for the target

5.6.3 The ARITH Compiler Option

IBM Enterprise COBOL provides the ARITH compiler option:

  • ARITH(COMPAT) (default): Intermediate results limited to 18 digits
  • ARITH(EXTEND): Intermediate results up to 31 digits

For financial calculations with large amounts and high-precision rates, ARITH(EXTEND) prevents intermediate truncation:

      *> With ARITH(COMPAT) — 18-digit limit:
      *> PIC S9(15)V99 * PIC SV9(6) could lose precision
      *> because the product needs 15+0=15 integer + 2+6=8 decimal
      *> = 23 digits, which exceeds 18

      *> With ARITH(EXTEND) — 31-digit limit:
      *> The full 23-digit intermediate is preserved

💡 Practical Advice: If your shop uses large monetary values (billions of dollars) with high-precision rates, request ARITH(EXTEND) from your systems programmer. Maria Chen enabled it for all GlobalBank programs in 2018 after discovering a truncation issue in the mortgage rate calculation module.

5.6.4 Controlling Precision with COMPUTE Restructuring

You can sometimes avoid intermediate precision problems by restructuring the expression:

      *--- Risky: large intermediate product ---
           COMPUTE WS-INTEREST =
               WS-PRINCIPAL * WS-RATE * WS-DAYS / 365

      *--- Safer: divide first to keep intermediate smaller ---
           COMPUTE WS-INTEREST =
               WS-PRINCIPAL * (WS-RATE * WS-DAYS / 365)

      *--- Safest: use explicit intermediate with known precision ---
           COMPUTE WS-DAILY-RATE = WS-RATE / 365
           COMPUTE WS-INTEREST =
               WS-PRINCIPAL * WS-DAILY-RATE * WS-DAYS

The third approach gives you full control over the intermediate precision because you define WS-DAILY-RATE with a known PIC clause.


5.7 Decimal vs. Binary vs. Floating Point

This section addresses the question that every financial programmer must understand: why do we insist on COMP-3 for money?

5.7.1 The Floating-Point Problem

COMP-1 (single precision) and COMP-2 (double precision) use IEEE 754 floating-point representation. In this format, numbers are stored as binary fractions, and many common decimal values cannot be represented exactly:

Decimal 0.1 in binary:
0.0001100110011001100110011... (repeating)

This is stored approximately as:
0.100000001490116... (COMP-1)
0.10000000000000000555... (COMP-2)

The error is tiny for a single value, but it accumulates:

       77  WS-FLOAT-SUM       COMP-1 VALUE ZERO.
       77  WS-PACK-SUM        PIC S9(7)V99 COMP-3 VALUE ZERO.
       77  WS-CTR             PIC 9(4) COMP.

           PERFORM VARYING WS-CTR FROM 1 BY 1
               UNTIL WS-CTR > 10000
               ADD 0.01 TO WS-FLOAT-SUM
               ADD 0.01 TO WS-PACK-SUM
           END-PERFORM

      *>   WS-PACK-SUM = 100.00 (exact)
      *>   WS-FLOAT-SUM ≈ 100.00393 (accumulated error!)

After just 10,000 additions of $0.01: - COMP-3 result: $100.00 (exact) - COMP-1 result: approximately $100.00393 (off by almost 4 cents)

For a bank processing 2.3 million transactions per day, the accumulated error would be catastrophic.

5.7.2 Why COMP-3 is Exact

Packed decimal (COMP-3) stores each decimal digit as a 4-bit value. The value 0.01 is stored as the nibbles 0 1 with appropriate decimal point positioning. This is an exact representation — no approximation, no repeating binary fractions.

Decimal 0.01 as COMP-3 (PIC SV99):
Stored as: | 01 | 0C |  (two nibbles for digits, one for sign)
This is EXACTLY 0.01. No error. No approximation.

Packed decimal arithmetic is also exact for addition, subtraction, and multiplication (within the field's precision). Division may produce a non-terminating result (like 1/3), but the truncation or rounding is explicit and controlled by your PIC clause and ROUNDED phrase.

5.7.3 The Binary (COMP) Compromise

COMP (binary) can represent all integers exactly, but it cannot represent decimal fractions exactly in the same way:

       77  WS-BIN-AMT         PIC S9(7)V99 COMP.

When you store 0.01 in a binary field with an implied decimal, the compiler stores it as the integer 1 and tracks the decimal position. Arithmetic is performed on the integer representation and then adjusted. This works correctly for all basic operations, but:

  1. The conversion from decimal to binary and back incurs a small CPU cost
  2. Intermediate multiplication/division results may lose precision if the binary intermediate cannot hold enough digits

For financial calculations, COMP-3 avoids these issues entirely.

5.7.4 Decision Matrix

┌──────────────────────────────────────────────────────────┐
│     USAGE Selection for Arithmetic                       │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  ALWAYS use COMP-3 when:                                 │
│    ✅ The field represents money                         │
│    ✅ The field participates in financial calculations    │
│    ✅ Exact decimal precision is required                 │
│    ✅ The field is stored in VSAM, DB2, or flat files     │
│                                                          │
│  Use COMP when:                                          │
│    ✅ The field is a pure integer (counter, subscript)    │
│    ✅ The field interfaces with system APIs               │
│    ✅ The field is used primarily as a loop variable       │
│                                                          │
│  Use COMP-2 ONLY when:                                   │
│    ✅ Scientific/engineering calculations are needed      │
│    ✅ The domain explicitly requires floating point       │
│    ⚠️  NEVER for financial/monetary values               │
│                                                          │
│  Avoid COMP-1 entirely:                                  │
│    ❌ 7 digits of precision is insufficient for almost   │
│       any serious calculation                            │
└──────────────────────────────────────────────────────────┘

5.8 Rounding Error Analysis

Understanding how rounding errors propagate through calculations is essential for designing correct financial programs.

5.8.1 Single-Step Rounding Error

When a result is rounded to fit a target field, the maximum error is:

Maximum single-step error = 0.5 * (smallest unit of the target field)

For PIC S9(7)V99 (two decimal places), the smallest unit is 0.01, so the maximum rounding error per operation is 0.005.

5.8.2 Accumulated Rounding Error

When multiple rounded operations are chained, errors accumulate. In the worst case:

Maximum accumulated error after N operations = N * 0.5 * (smallest unit)

For 12 monthly interest calculations rounded to cents:

Maximum accumulated error = 12 * 0.005 = 0.06 (six cents)

This is why financial systems often carry extra precision in intermediate calculations and round only at the final step:

       77  WS-MONTHLY-INT-PRECISE  PIC S9(7)V9(6) COMP-3.
       77  WS-ANNUAL-INT-PRECISE   PIC S9(9)V9(6) COMP-3.
       77  WS-ANNUAL-INT-ROUNDED   PIC S9(9)V99   COMP-3.

      *--- Calculate each month with full precision ---
           MOVE ZERO TO WS-ANNUAL-INT-PRECISE
           PERFORM VARYING WS-MONTH FROM 1 BY 1
               UNTIL WS-MONTH > 12
               COMPUTE WS-MONTHLY-INT-PRECISE =
                   WS-BALANCE(WS-MONTH) * WS-ANNUAL-RATE / 12
               ADD WS-MONTHLY-INT-PRECISE
                   TO WS-ANNUAL-INT-PRECISE
           END-PERFORM

      *--- Round only at the end ---
           COMPUTE WS-ANNUAL-INT-ROUNDED ROUNDED =
               WS-ANNUAL-INT-PRECISE

💡 Best Practice: Carry extra decimal places in intermediate calculations and round only the final result. Maria Chen calls this "the six-decimal rule" at GlobalBank — all intermediate interest calculations use PIC V9(6) regardless of the final reporting precision.


5.9 GlobalBank Case Study: The BAL-CALC Program

The BAL-CALC (Balance Calculation) program at GlobalBank computes daily interest on every account and posts it monthly. Let us walk through the critical arithmetic:

      ******************************************************************
      *  BAL-CALC — Daily Interest Calculation (simplified)
      *  Calculates daily accrued interest using actual/365 method
      ******************************************************************

       01  WS-CALC-FIELDS.
           05  WS-DAILY-RATE       PIC SV9(9) COMP-3.
           05  WS-DAILY-INTEREST   PIC S9(9)V9(6) COMP-3.
           05  WS-ACCRUED-INT      PIC S9(9)V9(6) COMP-3.
           05  WS-POSTED-INT       PIC S9(9)V99 COMP-3.
           05  WS-ROUNDING-ADJ     PIC S9(3)V9(6) COMP-3.

       01  WS-ACCOUNT-FIELDS.
           05  WS-BALANCE          PIC S9(11)V99 COMP-3.
           05  WS-ANNUAL-RATE      PIC S9V9(6)   COMP-3.
           05  WS-DAYS-IN-MONTH    PIC 9(2)      COMP.

       PROCEDURE DIVISION.
       CALC-DAILY-INTEREST.
      *--- Step 1: Compute daily rate with maximum precision ---
           COMPUTE WS-DAILY-RATE =
               WS-ANNUAL-RATE / 365
               ON SIZE ERROR
                   PERFORM HANDLE-CALC-ERROR
           END-COMPUTE

      *--- Step 2: Calculate each day's interest ---
           MOVE ZERO TO WS-ACCRUED-INT
           PERFORM VARYING WS-DAY FROM 1 BY 1
               UNTIL WS-DAY > WS-DAYS-IN-MONTH

               COMPUTE WS-DAILY-INTEREST =
                   WS-BALANCE * WS-DAILY-RATE
                   ON SIZE ERROR
                       PERFORM HANDLE-CALC-ERROR
               END-COMPUTE

               ADD WS-DAILY-INTEREST TO WS-ACCRUED-INT

           END-PERFORM

      *--- Step 3: Round for posting (only at the end) ---
           COMPUTE WS-POSTED-INT ROUNDED =
               WS-ACCRUED-INT
               ON SIZE ERROR
                   PERFORM HANDLE-CALC-ERROR
           END-COMPUTE

      *--- Step 4: Track rounding adjustment for audit ---
           COMPUTE WS-ROUNDING-ADJ =
               WS-ACCRUED-INT - WS-POSTED-INT

Notice the key patterns:

  1. WS-DAILY-RATE has 9 decimal places — far more than needed for the final result, but essential for intermediate precision
  2. WS-DAILY-INTEREST and WS-ACCRUED-INT have 6 decimal places — carrying extra precision through the accumulation loop
  3. WS-POSTED-INT has only 2 decimal places with ROUNDED — rounding occurs only at the very end
  4. WS-ROUNDING-ADJ tracks the rounding difference — essential for audit trails
  5. Every COMPUTE has ON SIZE ERROR — defensive programming throughout

Derek Washington asks: "Why not just round each daily interest to two decimal places before adding?" Maria's answer: "Because 0.005 times 30 days is 0.15. Rounding daily could cost or gain the customer up to 15 cents a month. Multiply that by 2.3 million accounts and you have a $345,000 monthly discrepancy. The auditors would not be pleased."


5.10 Financial Calculation Patterns

5.10.1 The Penny Distribution Problem

One of the most common financial programming challenges is distributing a total across multiple recipients so that every penny is accounted for:

      ******************************************************************
      *  PENNY-DIST — Distribute total evenly with remainder handling
      ******************************************************************
       01  WS-DIST-FIELDS.
           05  WS-TOTAL-AMT       PIC S9(9)V99 COMP-3.
           05  WS-NUM-SHARES      PIC 9(3)     COMP.
           05  WS-PER-SHARE       PIC S9(9)V99 COMP-3.
           05  WS-REMAINDER       PIC S9(3)V99 COMP-3.
           05  WS-SHARE-TABLE.
               10  WS-SHARE-AMT   PIC S9(9)V99 COMP-3
                                   OCCURS 100 TIMES.
           05  WS-VERIFICATION    PIC S9(9)V99 COMP-3.

       PROCEDURE DIVISION.
       DISTRIBUTE-EVENLY.
      *--- Calculate base share and remainder ---
           DIVIDE WS-TOTAL-AMT BY WS-NUM-SHARES
               GIVING WS-PER-SHARE
               REMAINDER WS-REMAINDER
               ON SIZE ERROR
                   PERFORM HANDLE-DIST-ERROR
           END-DIVIDE

      *--- Assign base share to all recipients ---
           PERFORM VARYING WS-IDX FROM 1 BY 1
               UNTIL WS-IDX > WS-NUM-SHARES
               MOVE WS-PER-SHARE TO WS-SHARE-AMT(WS-IDX)
           END-PERFORM

      *--- Distribute remainder pennies (one per recipient) ---
      *>   Convert remainder to pennies
           COMPUTE WS-REMAINDER-PENNIES =
               WS-REMAINDER * 100

           PERFORM VARYING WS-IDX FROM 1 BY 1
               UNTIL WS-IDX > WS-REMAINDER-PENNIES
               ADD 0.01 TO WS-SHARE-AMT(WS-IDX)
           END-PERFORM

      *--- Verify: sum of shares must equal total ---
           MOVE ZERO TO WS-VERIFICATION
           PERFORM VARYING WS-IDX FROM 1 BY 1
               UNTIL WS-IDX > WS-NUM-SHARES
               ADD WS-SHARE-AMT(WS-IDX)
                   TO WS-VERIFICATION
           END-PERFORM

           IF WS-VERIFICATION NOT EQUAL WS-TOTAL-AMT
               DISPLAY 'DISTRIBUTION ERROR: '
                   WS-VERIFICATION ' <> ' WS-TOTAL-AMT
               PERFORM HANDLE-DIST-ERROR
           END-IF.

This pattern ensures that SUM(shares) = total with zero discrepancy. The first N recipients (where N = remainder in pennies) each receive one extra penny.

5.10.2 Compound Interest Calculation

      ******************************************************************
      *  Compound interest: A = P * (1 + r/n)^(n*t)
      *  Where: P=principal, r=annual rate, n=compounds/year, t=years
      ******************************************************************
       01  WS-COMPOUND-FIELDS.
           05  WS-CI-PRINCIPAL     PIC S9(11)V99 COMP-3.
           05  WS-CI-RATE          PIC SV9(8)    COMP-3.
           05  WS-CI-COMPOUNDS     PIC 9(3)      COMP.
           05  WS-CI-YEARS         PIC 9(3)      COMP.
           05  WS-CI-PERIOD-RATE   PIC SV9(12)   COMP-3.
           05  WS-CI-TOTAL-PERIODS PIC 9(5)      COMP.
           05  WS-CI-GROWTH-FACTOR PIC S9(5)V9(12) COMP-3.
           05  WS-CI-FINAL-AMT     PIC S9(13)V99 COMP-3.
           05  WS-CI-INTEREST-EARNED PIC S9(13)V99 COMP-3.

       PROCEDURE DIVISION.
       CALC-COMPOUND-INTEREST.
      *--- Period rate = annual rate / compounds per year ---
           COMPUTE WS-CI-PERIOD-RATE =
               WS-CI-RATE / WS-CI-COMPOUNDS
               ON SIZE ERROR
                   PERFORM HANDLE-CI-ERROR
           END-COMPUTE

      *--- Total periods = compounds per year * years ---
           COMPUTE WS-CI-TOTAL-PERIODS =
               WS-CI-COMPOUNDS * WS-CI-YEARS

      *--- Growth factor = (1 + period rate) ^ total periods ---
      *>   NOTE: Exponentiation with large exponents may need
      *>   iterative calculation for full COMP-3 precision
           COMPUTE WS-CI-GROWTH-FACTOR =
               (1 + WS-CI-PERIOD-RATE)
               ** WS-CI-TOTAL-PERIODS
               ON SIZE ERROR
                   PERFORM HANDLE-CI-ERROR
           END-COMPUTE

      *--- Final amount = principal * growth factor ---
           COMPUTE WS-CI-FINAL-AMT ROUNDED =
               WS-CI-PRINCIPAL * WS-CI-GROWTH-FACTOR
               ON SIZE ERROR
                   PERFORM HANDLE-CI-ERROR
           END-COMPUTE

      *--- Interest earned = final - principal ---
           SUBTRACT WS-CI-PRINCIPAL FROM WS-CI-FINAL-AMT
               GIVING WS-CI-INTEREST-EARNED.

Notice: WS-CI-PERIOD-RATE has 12 decimal places and WS-CI-GROWTH-FACTOR has 12 decimal places to preserve precision through the exponentiation. The final result is rounded to cents only at the last step.

5.10.3 Loan Amortization Schedule

      ******************************************************************
      *  Monthly payment = P * [r(1+r)^n] / [(1+r)^n - 1]
      *  Where: P=principal, r=monthly rate, n=number of payments
      ******************************************************************
       01  WS-AMORT-FIELDS.
           05  WS-AM-PRINCIPAL     PIC S9(11)V99  COMP-3.
           05  WS-AM-ANNUAL-RATE   PIC SV9(8)     COMP-3.
           05  WS-AM-MONTHLY-RATE  PIC SV9(10)    COMP-3.
           05  WS-AM-NUM-PAYMENTS  PIC 9(4)       COMP.
           05  WS-AM-PAYMENT       PIC S9(9)V99   COMP-3.
           05  WS-AM-BALANCE       PIC S9(11)V99  COMP-3.
           05  WS-AM-INT-PORTION   PIC S9(9)V9(6) COMP-3.
           05  WS-AM-INT-ROUNDED   PIC S9(9)V99   COMP-3.
           05  WS-AM-PRIN-PORTION  PIC S9(9)V99   COMP-3.
           05  WS-AM-GROWTH-FACTOR PIC S9(5)V9(12) COMP-3.
           05  WS-AM-NUMERATOR     PIC S9(13)V9(12) COMP-3.
           05  WS-AM-DENOMINATOR   PIC S9(5)V9(12) COMP-3.

       PROCEDURE DIVISION.
       CALC-MONTHLY-PAYMENT.
           COMPUTE WS-AM-MONTHLY-RATE =
               WS-AM-ANNUAL-RATE / 12

           COMPUTE WS-AM-GROWTH-FACTOR =
               (1 + WS-AM-MONTHLY-RATE)
               ** WS-AM-NUM-PAYMENTS
               ON SIZE ERROR
                   PERFORM HANDLE-AMORT-ERROR
           END-COMPUTE

           COMPUTE WS-AM-NUMERATOR =
               WS-AM-PRINCIPAL * WS-AM-MONTHLY-RATE
               * WS-AM-GROWTH-FACTOR

           COMPUTE WS-AM-DENOMINATOR =
               WS-AM-GROWTH-FACTOR - 1

           COMPUTE WS-AM-PAYMENT ROUNDED =
               WS-AM-NUMERATOR / WS-AM-DENOMINATOR
               ON SIZE ERROR
                   PERFORM HANDLE-AMORT-ERROR
           END-COMPUTE.

       GENERATE-SCHEDULE.
           MOVE WS-AM-PRINCIPAL TO WS-AM-BALANCE
           PERFORM VARYING WS-PERIOD FROM 1 BY 1
               UNTIL WS-PERIOD > WS-AM-NUM-PAYMENTS

      *>       Interest for this period (full precision)
               COMPUTE WS-AM-INT-PORTION =
                   WS-AM-BALANCE * WS-AM-MONTHLY-RATE

      *>       Round interest to cents
               COMPUTE WS-AM-INT-ROUNDED ROUNDED =
                   WS-AM-INT-PORTION

      *>       Principal = payment - interest
               SUBTRACT WS-AM-INT-ROUNDED FROM WS-AM-PAYMENT
                   GIVING WS-AM-PRIN-PORTION

      *>       Last payment adjustment
               IF WS-PERIOD = WS-AM-NUM-PAYMENTS
                   MOVE WS-AM-BALANCE TO WS-AM-PRIN-PORTION
                   ADD WS-AM-INT-ROUNDED WS-AM-PRIN-PORTION
                       GIVING WS-AM-PAYMENT
               END-IF

      *>       Reduce balance
               SUBTRACT WS-AM-PRIN-PORTION FROM WS-AM-BALANCE

               PERFORM DISPLAY-AMORT-LINE
           END-PERFORM.

The last-payment adjustment is critical: due to rounding, the sum of all regular principal portions may not exactly equal the original principal. The final payment is adjusted so the balance reaches exactly zero.


5.11 MedClaim Case Study: Claim Proration

At MedClaim, claims often need to be prorated across multiple benefit categories. Here is the core proration logic:

      ******************************************************************
      *  CLAIM-PRORATE — Distribute claim amount across benefit tiers
      ******************************************************************
       01  WS-PRORATE-FIELDS.
           05  WS-PR-TOTAL-CHARGE   PIC S9(7)V99 COMP-3.
           05  WS-PR-ALLOWED-AMT    PIC S9(7)V99 COMP-3.
           05  WS-PR-DEDUCTIBLE     PIC S9(5)V99 COMP-3.
           05  WS-PR-COPAY          PIC S9(5)V99 COMP-3.
           05  WS-PR-COINSURANCE-PCT PIC V99      COMP-3.
           05  WS-PR-AFTER-DEDUCT   PIC S9(7)V99 COMP-3.
           05  WS-PR-PLAN-PAYS      PIC S9(7)V99 COMP-3.
           05  WS-PR-MEMBER-PAYS    PIC S9(7)V99 COMP-3.
           05  WS-PR-VERIFICATION   PIC S9(7)V99 COMP-3.

       PROCEDURE DIVISION.
       PRORATE-CLAIM.
      *--- Step 1: Start with allowed amount ---
      *>   (not billed charge — the plan has negotiated rates)
           MOVE WS-PR-ALLOWED-AMT TO WS-PR-AFTER-DEDUCT

      *--- Step 2: Apply deductible ---
           IF WS-PR-DEDUCTIBLE > ZERO
               IF WS-PR-AFTER-DEDUCT > WS-PR-DEDUCTIBLE
                   SUBTRACT WS-PR-DEDUCTIBLE
                       FROM WS-PR-AFTER-DEDUCT
               ELSE
                   MOVE WS-PR-AFTER-DEDUCT
                       TO WS-PR-DEDUCTIBLE
                   MOVE ZERO TO WS-PR-AFTER-DEDUCT
               END-IF
           END-IF

      *--- Step 3: Apply copay ---
           IF WS-PR-COPAY > ZERO
               IF WS-PR-AFTER-DEDUCT > WS-PR-COPAY
                   SUBTRACT WS-PR-COPAY
                       FROM WS-PR-AFTER-DEDUCT
               ELSE
                   MOVE WS-PR-AFTER-DEDUCT TO WS-PR-COPAY
                   MOVE ZERO TO WS-PR-AFTER-DEDUCT
               END-IF
           END-IF

      *--- Step 4: Apply coinsurance ---
           COMPUTE WS-PR-PLAN-PAYS ROUNDED =
               WS-PR-AFTER-DEDUCT *
               (1 - WS-PR-COINSURANCE-PCT)
               ON SIZE ERROR
                   PERFORM HANDLE-PRORATE-ERROR
           END-COMPUTE

      *--- Step 5: Member responsibility ---
           COMPUTE WS-PR-MEMBER-PAYS =
               WS-PR-ALLOWED-AMT - WS-PR-PLAN-PAYS

      *--- Step 6: Verification ---
           ADD WS-PR-PLAN-PAYS WS-PR-MEMBER-PAYS
               GIVING WS-PR-VERIFICATION

           IF WS-PR-VERIFICATION NOT = WS-PR-ALLOWED-AMT
               DISPLAY 'PRORATION MISMATCH: '
                   WS-PR-VERIFICATION ' vs '
                   WS-PR-ALLOWED-AMT
      *>       Adjust member portion to reconcile
               COMPUTE WS-PR-MEMBER-PAYS =
                   WS-PR-ALLOWED-AMT - WS-PR-PLAN-PAYS
           END-IF.

Notice the verification step — after all proration calculations, the program verifies that plan-pays + member-pays = allowed-amount. If rounding causes a discrepancy, the member portion is adjusted. Sarah Kim explains: "The plan amount is rounded once and locked. Any rounding discrepancy goes to the member responsibility. Our auditors require this — the plan's liability must be calculated first and must be exact."


5.12 Common Arithmetic Bugs and How to Avoid Them

Bug 1: Missing SIGN (S) in PIC

      *--- Bug ---
       77  WS-DIFFERENCE      PIC 9(7)V99 COMP-3.
           SUBTRACT WS-LARGER FROM WS-SMALLER
               GIVING WS-DIFFERENCE
      *>   If SMALLER < LARGER, the negative result loses its sign!

      *--- Fix ---
       77  WS-DIFFERENCE      PIC S9(7)V99 COMP-3.

Bug 2: No ON SIZE ERROR on DIVIDE

      *--- Bug ---
           DIVIDE WS-TOTAL BY WS-COUNT GIVING WS-AVERAGE
      *>   If WS-COUNT is zero, undefined behavior (often S0C7 abend)

      *--- Fix ---
           IF WS-COUNT > ZERO
               DIVIDE WS-TOTAL BY WS-COUNT
                   GIVING WS-AVERAGE ROUNDED
                   ON SIZE ERROR
                       PERFORM HANDLE-OVERFLOW
               END-DIVIDE
           ELSE
               MOVE ZERO TO WS-AVERAGE
           END-IF

Bug 3: Premature Rounding

      *--- Bug: Rounding each intermediate step ---
           COMPUTE WS-STEP1 ROUNDED = WS-AMT * WS-RATE
           COMPUTE WS-STEP2 ROUNDED = WS-STEP1 * WS-FACTOR
           COMPUTE WS-FINAL ROUNDED = WS-STEP2 / WS-DIVISOR
      *>   Each ROUNDED introduces up to 0.005 error

      *--- Fix: Round only the final result ---
           COMPUTE WS-FINAL ROUNDED =
               WS-AMT * WS-RATE * WS-FACTOR / WS-DIVISOR

Bug 4: Insufficient Target Field Size

      *--- Bug ---
       77  WS-RESULT          PIC S9(5)V99 COMP-3.
           COMPUTE WS-RESULT = 50000.00 * 12
      *>   Result 600000.00 exceeds PIC S9(5)V99 (max 99999.99)
      *>   Without SIZE ERROR, silently truncates!

      *--- Fix ---
       77  WS-RESULT          PIC S9(7)V99 COMP-3.
      *>   Or add ON SIZE ERROR to catch overflow

Bug 5: MULTIPLY BY Modifying the Wrong Operand

      *--- Confusing: which field is modified? ---
           MULTIPLY WS-QTY BY WS-PRICE
      *>   WS-PRICE is modified (not WS-QTY)!

      *--- Clear: GIVING makes the target explicit ---
           MULTIPLY WS-QTY BY WS-PRICE
               GIVING WS-TOTAL

Bug 6: Floating-Point for Financial Data

      *--- Bug ---
       77  WS-BALANCE         COMP-2.
      *>   NEVER use floating point for money

      *--- Fix ---
       77  WS-BALANCE         PIC S9(11)V99 COMP-3.

5.13 Arithmetic Performance Considerations

5.13.1 COMP-3 vs. COMP Performance

On IBM z/Architecture processors:

  • COMP-3 arithmetic uses dedicated packed decimal instructions (AP, SP, MP, DP). These execute in hardware and are very fast.
  • COMP arithmetic uses binary instructions (A, S, M, D). These are also fast but require conversion when the data is stored as packed decimal or display.

Rule of thumb: Use the USAGE that matches the data's primary purpose. If a field is primarily used in arithmetic and rarely displayed, COMP may be faster. If a field is stored in files as packed decimal and used in arithmetic, COMP-3 avoids conversion overhead.

5.13.2 COMPUTE vs. Explicit Verbs

For simple operations, there is no significant performance difference between COMPUTE and explicit verbs — the compiler generates equivalent instructions.

For complex expressions, COMPUTE can be more efficient because the compiler can optimize the entire expression as a unit, reusing intermediate registers and minimizing conversions.

5.13.3 The Performance of ON SIZE ERROR

ON SIZE ERROR adds a small overhead — the compiler must check for overflow after each operation. However, this overhead is negligible compared to the cost of a production bug. As Priya Kapoor says: "I'll take a microsecond of checking over a million-dollar reconciliation error."


5.14 Worked Example: Complete Interest Calculation Program

Let us put everything together in a complete, production-quality program:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. INTCALC.
      ******************************************************************
      *  INTCALC — Interest Calculation Program
      *  Demonstrates: COMPUTE, ROUNDED, ON SIZE ERROR,
      *  intermediate precision, penny distribution
      ******************************************************************

       DATA DIVISION.
       WORKING-STORAGE SECTION.

       01  WS-ACCOUNT.
           05  WS-ACCT-NUM         PIC 9(10).
           05  WS-ACCT-BALANCE     PIC S9(11)V99 COMP-3.
           05  WS-ACCT-RATE        PIC SV9(6)    COMP-3.
           05  WS-ACCT-INT-YTD     PIC S9(9)V99  COMP-3.

       01  WS-CALC.
           05  WS-DAILY-RATE       PIC SV9(9)    COMP-3.
           05  WS-DAILY-INTEREST   PIC S9(9)V9(6) COMP-3.
           05  WS-MONTH-INTEREST   PIC S9(9)V9(6) COMP-3.
           05  WS-POSTED-INTEREST  PIC S9(9)V99   COMP-3.
           05  WS-ROUND-DIFF       PIC S9(3)V9(6) COMP-3.

       01  WS-WORK.
           05  WS-DAYS-IN-MONTH    PIC 9(2)       COMP.
           05  WS-DAY-CTR          PIC 9(2)       COMP.

       01  WS-DISPLAY.
           05  WS-DSP-BALANCE      PIC $$$,$$$,$$$,$$9.99-.
           05  WS-DSP-RATE         PIC Z9.9(6).
           05  WS-DSP-INTEREST     PIC $$,$$$,$$9.99-.
           05  WS-DSP-DIFF         PIC -(3)9.9(6).

       01  WS-FLAGS.
           05  WS-CALC-STATUS      PIC X.
               88  WS-CALC-OK         VALUE 'O'.
               88  WS-CALC-ERROR      VALUE 'E'.

       PROCEDURE DIVISION.
       MAIN-PROGRAM.
      *--- Set up sample account ---
           MOVE 1234567890     TO WS-ACCT-NUM
           MOVE 250000.00      TO WS-ACCT-BALANCE
           MOVE .0425          TO WS-ACCT-RATE
           MOVE ZERO           TO WS-ACCT-INT-YTD
           MOVE 30             TO WS-DAYS-IN-MONTH
           SET WS-CALC-OK TO TRUE

      *--- Calculate interest ---
           PERFORM CALCULATE-MONTHLY-INTEREST

      *--- Display results ---
           IF WS-CALC-OK
               PERFORM DISPLAY-RESULTS
           ELSE
               DISPLAY 'CALCULATION ERROR OCCURRED'
           END-IF

           STOP RUN.

       CALCULATE-MONTHLY-INTEREST.
      *--- Step 1: Daily rate with maximum precision ---
           COMPUTE WS-DAILY-RATE =
               WS-ACCT-RATE / 365
               ON SIZE ERROR
                   SET WS-CALC-ERROR TO TRUE
           END-COMPUTE

           IF WS-CALC-ERROR
               EXIT PARAGRAPH
           END-IF

      *--- Step 2: Accumulate daily interest ---
           MOVE ZERO TO WS-MONTH-INTEREST
           PERFORM VARYING WS-DAY-CTR FROM 1 BY 1
               UNTIL WS-DAY-CTR > WS-DAYS-IN-MONTH
               OR WS-CALC-ERROR

               COMPUTE WS-DAILY-INTEREST =
                   WS-ACCT-BALANCE * WS-DAILY-RATE
                   ON SIZE ERROR
                       SET WS-CALC-ERROR TO TRUE
               END-COMPUTE

               IF WS-CALC-OK
                   ADD WS-DAILY-INTEREST
                       TO WS-MONTH-INTEREST
               END-IF
           END-PERFORM

      *--- Step 3: Round for posting ---
           IF WS-CALC-OK
               COMPUTE WS-POSTED-INTEREST ROUNDED =
                   WS-MONTH-INTEREST
                   ON SIZE ERROR
                       SET WS-CALC-ERROR TO TRUE
               END-COMPUTE
           END-IF

      *--- Step 4: Track rounding difference ---
           IF WS-CALC-OK
               COMPUTE WS-ROUND-DIFF =
                   WS-MONTH-INTEREST - WS-POSTED-INTEREST
           END-IF

      *--- Step 5: Apply to account ---
           IF WS-CALC-OK
               ADD WS-POSTED-INTEREST TO WS-ACCT-BALANCE
               ADD WS-POSTED-INTEREST TO WS-ACCT-INT-YTD
           END-IF.

       DISPLAY-RESULTS.
           MOVE WS-ACCT-BALANCE TO WS-DSP-BALANCE
           MOVE WS-ACCT-RATE    TO WS-DSP-RATE
           MOVE WS-POSTED-INTEREST TO WS-DSP-INTEREST
           MOVE WS-ROUND-DIFF   TO WS-DSP-DIFF

           DISPLAY '=== INTEREST CALCULATION RESULTS ==='
           DISPLAY 'Account:       ' WS-ACCT-NUM
           DISPLAY 'Balance:       ' WS-DSP-BALANCE
           DISPLAY 'Annual Rate:   ' WS-DSP-RATE '%'
           DISPLAY 'Days:          ' WS-DAYS-IN-MONTH
           DISPLAY 'Monthly Int:   ' WS-DSP-INTEREST
           DISPLAY 'Rounding Diff: ' WS-DSP-DIFF.

🧪 Try It Yourself: Extend the Interest Calculator

  1. Modify the program to read account data from a sequential file instead of hard-coded values
  2. Add a report that shows the daily interest for each day of the month
  3. Implement the penny distribution pattern for a case where interest must be split between a checking and savings account in a 60/40 ratio
  4. Add NEAREST-EVEN rounding (if your compiler supports it) and compare the results with standard rounding for 1,000 sample accounts

5.15 COBOL 2002+ Arithmetic Enhancements

Modern COBOL standards have added several arithmetic features:

5.15.1 ROUNDED MODE Clause

As discussed in Section 5.4.4, COBOL 2002 adds explicit rounding mode selection:

           COMPUTE WS-RESULT
               ROUNDED MODE IS NEAREST-EVEN
               = WS-AMOUNT / WS-DIVISOR

5.15.2 ARITHMETIC Clause in COMPUTE

Some compilers support an ARITHMETIC clause that specifies the arithmetic model:

           COMPUTE WS-RESULT = expression
               ARITHMETIC IS STANDARD
      *>   Uses the standard COBOL arithmetic model with
      *>   maximum intermediate precision

5.15.3 Extended Precision

With ARITH(EXTEND) on IBM compilers, numeric literals and intermediate results can have up to 31 digits instead of the traditional 18. This is essential for calculations involving very large numbers or very precise rates.


5.16 The Penny Rounding Problem in Detail

The penny rounding problem is one of the most subtle and consequential challenges in financial computing. It deserves its own detailed treatment because it has caused real financial discrepancies at institutions like GlobalBank.

5.16.1 The Problem Stated Precisely

When a dollar amount must be divided into N equal parts, the mathematical result often has infinite decimal expansion. For example, dividing $100.00 among three recipients:

$100.00 / 3 = $33.333333...

Rounded to cents: $33.33 per recipient. But $33.33 * 3 = $99.99. There is a missing penny. Where did it go?

The answer is nowhere — it was lost to rounding. This penny must be accounted for, and the distribution algorithm must decide who gets it.

5.16.2 Three Distribution Strategies

Strategy 1: First Recipient Gets the Remainder

       01  WS-DIST-FIELDS.
           05  WS-TOTAL             PIC S9(9)V99 COMP-3.
           05  WS-PARTS             PIC 9(3)     COMP.
           05  WS-BASE-SHARE        PIC S9(9)V99 COMP-3.
           05  WS-FIRST-SHARE       PIC S9(9)V99 COMP-3.
           05  WS-CHECK-TOTAL       PIC S9(9)V99 COMP-3.

      *--- Calculate truncated base share ---
           DIVIDE WS-TOTAL BY WS-PARTS
               GIVING WS-BASE-SHARE
      *>       No ROUNDED — intentional truncation

      *--- First recipient gets the rest ---
           COMPUTE WS-FIRST-SHARE =
               WS-TOTAL - (WS-BASE-SHARE * (WS-PARTS - 1))

      *--- Verify ---
           COMPUTE WS-CHECK-TOTAL =
               WS-FIRST-SHARE +
               (WS-BASE-SHARE * (WS-PARTS - 1))
      *>   WS-CHECK-TOTAL always equals WS-TOTAL

For $100.00 / 3: First recipient gets $33.34, other two get $33.33 each. Total: $100.00 exactly.

Strategy 2: Round-Robin Penny Distribution

This is the pattern shown earlier in Section 5.10.1. The remainder (in pennies) is distributed one cent at a time across the first N recipients. This is more equitable when distributing among many recipients:

$100.00 / 3 → base $33.33, remainder $0.01
Recipient 1: $33.34 (gets the extra penny)
Recipient 2: $33.33
Recipient 3: $33.33
Total: $100.00 ✓

Strategy 3: Largest Remainder Method

Used in some regulatory contexts, this method distributes extra pennies to the recipients whose truncated shares had the largest fractional part. For $100.00 / 3, all three have the same fraction (0.333...), so the choice is arbitrary. But for $100.00 / 7:

$100.00 / 7 = $14.285714...
Base share (truncated): $14.28
Total of 7 * $14.28 = $99.96
Remainder: $0.04 (4 pennies to distribute)

Fractional part: .005714...
All recipients have the same fraction, so first 4 get the penny.
Recipients 1-4: $14.29, Recipients 5-7: $14.28
Total: 4 * $14.29 + 3 * $14.28 = $57.16 + $42.84 = $100.00 ✓

5.16.3 Percentage-Based Distribution

A more complex scenario arises when amounts must be distributed by percentage. At MedClaim, a $1,000.00 claim might be split:

  • Provider A: 45% = $450.00
  • Provider B: 35% = $350.00
  • Provider C: 20% = $200.00

That works cleanly. But consider $997.53 split 33.33% / 33.33% / 33.34%:

33.33% of $997.53 = $332.4472...  → rounded $332.45
33.33% of $997.53 = $332.4472...  → rounded $332.45
33.34% of $997.53 = $332.6350...  → rounded $332.64

Sum: $332.45 + $332.45 + $332.64 = $997.54 ≠ $997.53

The sum exceeds the original by one cent. The defensive pattern is to compute N-1 portions by percentage and derive the last portion by subtraction:

      *--- Compute first N-1 portions ---
           COMPUTE WS-PORTION-A ROUNDED =
               WS-TOTAL * WS-PCT-A
           COMPUTE WS-PORTION-B ROUNDED =
               WS-TOTAL * WS-PCT-B

      *--- Last portion absorbs rounding ---
           COMPUTE WS-PORTION-C =
               WS-TOTAL - WS-PORTION-A - WS-PORTION-B

      *--- WS-PORTION-A + WS-PORTION-B + WS-PORTION-C
      *--- always equals WS-TOTAL exactly ---

James Okafor at MedClaim calls this the "last-man-standing" rule: "The last portion is never computed from a percentage — it is always derived by subtraction from the total. That way, the sum is guaranteed to balance."

5.16.4 Regulatory Implications

Financial regulators require exact penny reconciliation. In the United States:

  • SEC Rule 10b-10 requires broker-dealers to disclose exact commission amounts
  • IRS regulations require tax withholding calculations to the penny
  • GAAP accounting standards require trial balances to zero

A system that routinely loses or gains pennies will fail audit. Maria Chen's team at GlobalBank runs a daily reconciliation job that detects any account where the sum of transaction postings does not equal the net balance change. "We have zero tolerance for penny discrepancies," she says. "When we find one, it is always an arithmetic bug in a COBOL program."

🧪 Try It Yourself: Penny Distribution Challenge

Write a COBOL program that distributes $10,000.00 among 7 departments according to these percentages: 18.5%, 14.3%, 12.7%, 15.1%, 11.9%, 13.8%, 13.7%. Verify that the total equals $10,000.00 exactly. Use the "last-man-standing" rule. Then modify the program to distribute $1.00 among 3 departments at 33.33%, 33.33%, 33.34% and confirm exact reconciliation.


5.17 Currency Calculations and Tax Computation Patterns

Financial COBOL programs must handle not only basic arithmetic but also the specific patterns required by tax law, currency conversion, and regulatory reporting.

5.17.1 Sales Tax Calculation

Sales tax seems simple — multiply by the rate. But the details matter:

       01  WS-TAX-FIELDS.
           05  WS-SUBTOTAL          PIC S9(7)V99 COMP-3.
           05  WS-TAX-RATE          PIC V9(6)    COMP-3.
           05  WS-TAX-AMT           PIC S9(7)V99 COMP-3.
           05  WS-TOTAL-DUE         PIC S9(7)V99 COMP-3.

      *--- Tax calculation with proper rounding ---
           COMPUTE WS-TAX-AMT ROUNDED =
               WS-SUBTOTAL * WS-TAX-RATE
               ON SIZE ERROR
                   PERFORM HANDLE-TAX-ERROR
           END-COMPUTE

      *--- Total must equal subtotal + tax exactly ---
           ADD WS-SUBTOTAL WS-TAX-AMT
               GIVING WS-TOTAL-DUE
               ON SIZE ERROR
                   PERFORM HANDLE-TAX-ERROR
           END-ADD

The key principle: compute tax by multiplying subtotal by rate with ROUNDED, then compute total by adding subtotal and tax (not by multiplying subtotal by 1 + rate). This ensures that total = subtotal + tax exactly.

5.17.2 Multi-Jurisdiction Tax

When an item is subject to multiple tax jurisdictions (state, county, city), each tax is computed separately on the pre-tax amount:

       01  WS-MULTI-TAX.
           05  WS-STATE-RATE        PIC V9(6) COMP-3.
           05  WS-COUNTY-RATE       PIC V9(6) COMP-3.
           05  WS-CITY-RATE         PIC V9(6) COMP-3.
           05  WS-STATE-TAX         PIC S9(7)V99 COMP-3.
           05  WS-COUNTY-TAX        PIC S9(7)V99 COMP-3.
           05  WS-CITY-TAX          PIC S9(7)V99 COMP-3.
           05  WS-TOTAL-TAX         PIC S9(7)V99 COMP-3.

      *--- Each tax computed on the base amount ---
           COMPUTE WS-STATE-TAX ROUNDED =
               WS-SUBTOTAL * WS-STATE-RATE
           COMPUTE WS-COUNTY-TAX ROUNDED =
               WS-SUBTOTAL * WS-COUNTY-RATE
           COMPUTE WS-CITY-TAX ROUNDED =
               WS-SUBTOTAL * WS-CITY-RATE

      *--- Total tax is the sum of individual taxes ---
           ADD WS-STATE-TAX WS-COUNTY-TAX WS-CITY-TAX
               GIVING WS-TOTAL-TAX

      *--- Do NOT compute total tax as:
      *>   SUBTOTAL * (STATE-RATE + COUNTY-RATE + CITY-RATE)
      *>   because the individual taxes would not be
      *>   derivable from the total, and reporting requires
      *>   each jurisdiction's tax separately.

⚠️ Why Separate Computation Matters: Tax authorities require reporting of their specific jurisdiction's tax, not a combined rate. Computing a combined rate and then trying to decompose it back into individual jurisdiction amounts introduces additional rounding errors.

5.17.3 Tiered Tax Rate Calculation

Progressive tax brackets require calculating tax at different rates for different portions of the amount. This is the pattern used for income tax withholding:

       01  WS-TAX-BRACKETS.
           05  WS-BRACKET-LIMIT     PIC S9(9)V99 COMP-3
                                    OCCURS 5 TIMES.
           05  WS-BRACKET-RATE      PIC V9(6) COMP-3
                                    OCCURS 5 TIMES.
       01  WS-TAX-WORK.
           05  WS-TAXABLE-INCOME    PIC S9(9)V99 COMP-3.
           05  WS-REMAINING         PIC S9(9)V99 COMP-3.
           05  WS-BRACKET-AMT       PIC S9(9)V99 COMP-3.
           05  WS-BRACKET-TAX       PIC S9(9)V9(6) COMP-3.
           05  WS-TOTAL-WTAX        PIC S9(9)V99 COMP-3.
           05  WS-PREV-LIMIT        PIC S9(9)V99 COMP-3.

       CALC-TIERED-TAX.
           MOVE WS-TAXABLE-INCOME TO WS-REMAINING
           MOVE ZERO TO WS-TOTAL-WTAX
           MOVE ZERO TO WS-PREV-LIMIT

           PERFORM VARYING WS-IDX FROM 1 BY 1
               UNTIL WS-IDX > 5
               OR WS-REMAINING <= ZERO

      *>       Amount taxable in this bracket
               COMPUTE WS-BRACKET-AMT =
                   FUNCTION MIN(WS-REMAINING,
                       WS-BRACKET-LIMIT(WS-IDX)
                       - WS-PREV-LIMIT)

      *>       Tax for this bracket (keep full precision)
               COMPUTE WS-BRACKET-TAX =
                   WS-BRACKET-AMT * WS-BRACKET-RATE(WS-IDX)

               ADD WS-BRACKET-TAX TO WS-TOTAL-WTAX
               SUBTRACT WS-BRACKET-AMT FROM WS-REMAINING
               MOVE WS-BRACKET-LIMIT(WS-IDX)
                   TO WS-PREV-LIMIT
           END-PERFORM

      *>   Round only at the end
           COMPUTE WS-TOTAL-WTAX ROUNDED = WS-TOTAL-WTAX

Notice that WS-BRACKET-TAX has 6 decimal places to carry full precision through the bracket loop. Rounding occurs only at the final step, after all bracket taxes have been accumulated.

5.17.4 Currency Conversion

When GlobalBank processes international wire transfers, amounts must be converted between currencies. The conversion introduces rounding issues because exchange rates have many decimal places:

       01  WS-FX-FIELDS.
           05  WS-SOURCE-AMT        PIC S9(11)V99 COMP-3.
           05  WS-FX-RATE           PIC 9(2)V9(8) COMP-3.
           05  WS-TARGET-AMT        PIC S9(13)V99 COMP-3.
           05  WS-INVERSE-CHECK     PIC S9(11)V99 COMP-3.

      *--- Convert USD to EUR ---
           COMPUTE WS-TARGET-AMT ROUNDED =
               WS-SOURCE-AMT * WS-FX-RATE
               ON SIZE ERROR
                   PERFORM HANDLE-FX-ERROR
           END-COMPUTE

      *--- Inverse check (for audit) ---
           IF WS-FX-RATE > ZERO
               COMPUTE WS-INVERSE-CHECK ROUNDED =
                   WS-TARGET-AMT / WS-FX-RATE
           END-IF
      *>   WS-INVERSE-CHECK may differ from WS-SOURCE-AMT
      *>   by up to 1 cent due to rounding — this is expected
      *>   and documented in the audit trail

The exchange rate field WS-FX-RATE has 8 decimal places because foreign exchange rates are quoted with high precision (e.g., 1 USD = 0.92148573 EUR). Maria Chen notes: "We carry 8 decimal places on FX rates because the ECB publishes rates to 6 places and we need headroom for triangulation through intermediate currencies."

📊 ARITH(EXTEND) and Currency: When converting very large amounts (billions of dollars/euros) with high-precision exchange rates, the intermediate result of WS-SOURCE-AMT * WS-FX-RATE may require more than 18 digits. This is precisely the scenario where ARITH(EXTEND) is essential:

$999,999,999.99 * 0.92148573 → product has 11+2+0+8 = 21 digits
ARITH(COMPAT) limit: 18 digits → TRUNCATION (silent precision loss!)
ARITH(EXTEND) limit: 31 digits → full precision preserved

5.18 Worked Example: Complete Payroll Tax Withholding

Let us combine several patterns into a realistic payroll withholding calculation. This program computes federal income tax, Social Security, and Medicare withholding for a single pay period:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. TAXCALC.
      ******************************************************************
      *  TAXCALC — Payroll Tax Withholding Calculator
      *  Demonstrates: tiered rates, multiple taxes, penny-exact
      *  reconciliation, ARITH precision awareness
      ******************************************************************

       DATA DIVISION.
       WORKING-STORAGE SECTION.

       01  WS-EMPLOYEE.
           05  WS-EMP-ID            PIC 9(8).
           05  WS-GROSS-PAY         PIC S9(7)V99 COMP-3.
           05  WS-YTD-GROSS         PIC S9(9)V99 COMP-3.
           05  WS-FILING-STATUS     PIC X(1).
               88  WS-SINGLE        VALUE 'S'.
               88  WS-MARRIED       VALUE 'M'.

       01  WS-WITHHOLDINGS.
           05  WS-FIT-TAX           PIC S9(7)V99   COMP-3.
           05  WS-SS-TAX            PIC S9(7)V99   COMP-3.
           05  WS-MED-TAX           PIC S9(7)V99   COMP-3.
           05  WS-TOTAL-WITH        PIC S9(7)V99   COMP-3.
           05  WS-NET-PAY           PIC S9(7)V99   COMP-3.

       01  WS-SS-CONSTANTS.
           05  WS-SS-RATE           PIC V9(4) COMP-3 VALUE .0620.
           05  WS-SS-WAGE-LIMIT     PIC S9(9)V99 COMP-3
                                    VALUE 168600.00.
           05  WS-MED-RATE          PIC V9(4) COMP-3 VALUE .0145.

       01  WS-WORK.
           05  WS-SS-TAXABLE        PIC S9(9)V99 COMP-3.
           05  WS-FIT-PRECISE       PIC S9(7)V9(6) COMP-3.
           05  WS-CHECK-TOTAL       PIC S9(9)V99 COMP-3.

       PROCEDURE DIVISION.
       MAIN-PROGRAM.
           MOVE 1001 TO WS-EMP-ID
           MOVE 5000.00 TO WS-GROSS-PAY
           MOVE 145000.00 TO WS-YTD-GROSS
           SET WS-SINGLE TO TRUE

           PERFORM CALC-SOCIAL-SECURITY
           PERFORM CALC-MEDICARE
           PERFORM CALC-FIT-SIMPLE

      *--- Net pay by subtraction (guaranteed exact) ---
           COMPUTE WS-NET-PAY =
               WS-GROSS-PAY - WS-FIT-TAX
                             - WS-SS-TAX
                             - WS-MED-TAX

      *--- Verification ---
           ADD WS-NET-PAY WS-FIT-TAX WS-SS-TAX WS-MED-TAX
               GIVING WS-CHECK-TOTAL
           IF WS-CHECK-TOTAL NOT = WS-GROSS-PAY
               DISPLAY 'RECONCILIATION ERROR'
           END-IF

           DISPLAY 'GROSS:    ' WS-GROSS-PAY
           DISPLAY 'FIT:      ' WS-FIT-TAX
           DISPLAY 'SS:       ' WS-SS-TAX
           DISPLAY 'MEDICARE: ' WS-MED-TAX
           DISPLAY 'NET:      ' WS-NET-PAY
           STOP RUN.

       CALC-SOCIAL-SECURITY.
      *--- SS tax applies only up to wage limit ---
           IF WS-YTD-GROSS >= WS-SS-WAGE-LIMIT
               MOVE ZERO TO WS-SS-TAX
           ELSE
               COMPUTE WS-SS-TAXABLE =
                   FUNCTION MIN(WS-GROSS-PAY,
                       WS-SS-WAGE-LIMIT - WS-YTD-GROSS)
               COMPUTE WS-SS-TAX ROUNDED =
                   WS-SS-TAXABLE * WS-SS-RATE
           END-IF.

       CALC-MEDICARE.
      *--- Medicare has no wage limit ---
           COMPUTE WS-MED-TAX ROUNDED =
               WS-GROSS-PAY * WS-MED-RATE.

       CALC-FIT-SIMPLE.
      *--- Simplified flat-rate for demonstration ---
           COMPUTE WS-FIT-TAX ROUNDED =
               WS-GROSS-PAY * .22.

The critical design principle is that net pay is computed by subtraction from gross, not by independent calculation. This guarantees Gross = FIT + SS + Medicare + Net with zero discrepancy.


5.19 The ARITH Compiler Option: Practical Implications

The ARITH compiler option deserves additional treatment because its effects are non-obvious and can cause programs that work correctly in one environment to silently produce wrong results in another.

5.19.1 ARITH(COMPAT) vs. ARITH(EXTEND) in Practice

Under ARITH(COMPAT) — the default — numeric literals, intermediate results, and the FUNCTION intrinsic functions are limited to 18 digits. Under ARITH(EXTEND), the limit increases to 31 digits. The practical consequences:

ARITH(COMPAT):
  Maximum numeric literal:  18 digits
  Maximum PIC 9 digits:     18
  Maximum intermediate:     18 digits
  FUNCTION RANDOM precision: ~15 significant digits

ARITH(EXTEND):
  Maximum numeric literal:  31 digits
  Maximum PIC 9 digits:     31
  Maximum intermediate:     31 digits
  FUNCTION RANDOM precision: ~28 significant digits

5.19.2 When ARITH(COMPAT) Silently Truncates

Consider a program that computes total assets across all GlobalBank accounts:

       77  WS-TOTAL-ASSETS      PIC S9(15)V99 COMP-3.
       77  WS-ACCOUNT-BAL       PIC S9(11)V99 COMP-3.
       77  WS-INTEREST-RATE     PIC SV9(8)    COMP-3.
       77  WS-PROJECTED-INT     PIC S9(15)V99 COMP-3.

           COMPUTE WS-PROJECTED-INT =
               WS-TOTAL-ASSETS * WS-INTEREST-RATE
      *>   Integer digits: 15 + 0 = 15
      *>   Decimal digits: 2 + 8 = 10
      *>   Total needed:   25 digits
      *>
      *>   ARITH(COMPAT): intermediate capped at 18 digits
      *>       -> 7 digits of precision LOST silently
      *>   ARITH(EXTEND): intermediate can hold 25 digits
      *>       -> full precision preserved

When WS-TOTAL-ASSETS is $500 billion (500,000,000,000.00) and the rate is 0.04250000, the true product is 21,250,000,000.0000000000. Under ARITH(COMPAT), the 25-digit intermediate is truncated to 18 digits, potentially losing the least significant 7 decimal digits. For this particular example, the final result (rounded to 2 decimal places) would still be correct. But for more complex expressions with multiple chained multiplications, the precision loss compounds.

5.19.3 Detecting ARITH Issues

Maria Chen's team uses a compile-time checklist for programs handling very large amounts:

  1. List all COMPUTE statements with multiplication
  2. For each, sum the integer digits and decimal digits of all operands
  3. If the total exceeds 18, flag for ARITH(EXTEND)
  4. For division, check whether the quotient + decimal precision exceeds 18

💡 Practical Rule: If your program handles amounts greater than $1 billion (PIC S9(13)V99 or larger) and multiplies them by rates with more than 4 decimal places, use ARITH(EXTEND). The cost is zero at runtime — ARITH is a compile-time option that simply tells the compiler to use wider intermediate fields.


5.20 Worked Example: Multi-Step Claim Payment with Full Audit Trail

Let us build a complete MedClaim payment calculation that demonstrates every defensive arithmetic pattern from this chapter. This program computes the plan payment for a medical claim, applying deductible, copay, coinsurance, and out-of-pocket maximums:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. CLMPAY01.
      ******************************************************************
      *  CLMPAY01 — Claim Payment Calculator with Full Audit Trail
      *  Demonstrates: Multi-step financial calculation, ROUNDED,
      *  ON SIZE ERROR, intermediate precision, verification
      ******************************************************************

       DATA DIVISION.
       WORKING-STORAGE SECTION.

       01  WS-CLAIM-INPUT.
           05  WS-BILLED-CHARGE     PIC S9(7)V99 COMP-3.
           05  WS-ALLOWED-AMT       PIC S9(7)V99 COMP-3.
           05  WS-DEDUCTIBLE-REMAINING PIC S9(5)V99 COMP-3.
           05  WS-OOP-REMAINING     PIC S9(7)V99 COMP-3.
           05  WS-COPAY             PIC S9(5)V99 COMP-3.
           05  WS-COINSURANCE-PCT   PIC V99       COMP-3.

       01  WS-CALC-RESULTS.
           05  WS-AFTER-DEDUCTIBLE  PIC S9(7)V99 COMP-3.
           05  WS-AFTER-COPAY       PIC S9(7)V99 COMP-3.
           05  WS-PLAN-SHARE        PIC S9(7)V99 COMP-3.
           05  WS-MEMBER-SHARE      PIC S9(7)V99 COMP-3.
           05  WS-PLAN-PAYS         PIC S9(7)V99 COMP-3.
           05  WS-MEMBER-PAYS       PIC S9(7)V99 COMP-3.
           05  WS-WRITEOFF          PIC S9(7)V99 COMP-3.

       01  WS-AUDIT-TRAIL.
           05  WS-STEP-1-AMT        PIC S9(7)V99 COMP-3.
           05  WS-STEP-2-AMT        PIC S9(7)V99 COMP-3.
           05  WS-STEP-3-AMT        PIC S9(7)V99 COMP-3.
           05  WS-VERIFY-TOTAL      PIC S9(7)V99 COMP-3.

       01  WS-ERROR-FLAG            PIC X VALUE 'N'.
           88  CALC-ERROR            VALUE 'Y'.

       PROCEDURE DIVISION.
       MAIN-PROGRAM.
      *--- Sample claim data ---
           MOVE 5000.00 TO WS-BILLED-CHARGE
           MOVE 3800.00 TO WS-ALLOWED-AMT
           MOVE 750.00  TO WS-DEDUCTIBLE-REMAINING
           MOVE 3000.00 TO WS-OOP-REMAINING
           MOVE 30.00   TO WS-COPAY
           MOVE .20     TO WS-COINSURANCE-PCT

           PERFORM CALCULATE-PAYMENT
           PERFORM VERIFY-CALCULATION
           PERFORM DISPLAY-AUDIT-TRAIL
           STOP RUN.

       CALCULATE-PAYMENT.
      *--- Step 1: Writeoff = Billed - Allowed ---
           COMPUTE WS-WRITEOFF =
               WS-BILLED-CHARGE - WS-ALLOWED-AMT
           MOVE WS-ALLOWED-AMT TO WS-STEP-1-AMT

      *--- Step 2: Apply deductible ---
           IF WS-DEDUCTIBLE-REMAINING >= WS-ALLOWED-AMT
               MOVE WS-ALLOWED-AMT TO WS-STEP-2-AMT
               MOVE ZERO TO WS-AFTER-DEDUCTIBLE
           ELSE
               MOVE WS-DEDUCTIBLE-REMAINING TO WS-STEP-2-AMT
               COMPUTE WS-AFTER-DEDUCTIBLE =
                   WS-ALLOWED-AMT - WS-DEDUCTIBLE-REMAINING
           END-IF

      *--- Step 3: Apply copay ---
           IF WS-AFTER-DEDUCTIBLE <= WS-COPAY
               MOVE WS-AFTER-DEDUCTIBLE TO WS-STEP-3-AMT
               MOVE ZERO TO WS-AFTER-COPAY
           ELSE
               MOVE WS-COPAY TO WS-STEP-3-AMT
               COMPUTE WS-AFTER-COPAY =
                   WS-AFTER-DEDUCTIBLE - WS-COPAY
           END-IF

      *--- Step 4: Apply coinsurance ---
           COMPUTE WS-PLAN-SHARE ROUNDED =
               WS-AFTER-COPAY * (1 - WS-COINSURANCE-PCT)
               ON SIZE ERROR
                   SET CALC-ERROR TO TRUE
           END-COMPUTE

      *--- Step 5: Member coinsurance by subtraction ---
           COMPUTE WS-MEMBER-SHARE =
               WS-AFTER-COPAY - WS-PLAN-SHARE

      *--- Step 6: Totals ---
           MOVE WS-PLAN-SHARE TO WS-PLAN-PAYS
           COMPUTE WS-MEMBER-PAYS =
               WS-STEP-2-AMT + WS-STEP-3-AMT
               + WS-MEMBER-SHARE.

       VERIFY-CALCULATION.
           ADD WS-PLAN-PAYS WS-MEMBER-PAYS WS-WRITEOFF
               GIVING WS-VERIFY-TOTAL
           IF WS-VERIFY-TOTAL NOT = WS-BILLED-CHARGE
               DISPLAY 'VERIFICATION FAILED: '
                   WS-VERIFY-TOTAL ' <> ' WS-BILLED-CHARGE
           END-IF.

       DISPLAY-AUDIT-TRAIL.
           DISPLAY '=== CLAIM PAYMENT AUDIT TRAIL ==='
           DISPLAY 'Billed Charge:   ' WS-BILLED-CHARGE
           DISPLAY 'Allowed Amount:  ' WS-ALLOWED-AMT
           DISPLAY 'Writeoff:        ' WS-WRITEOFF
           DISPLAY 'Deductible Used: ' WS-STEP-2-AMT
           DISPLAY 'Copay Applied:   ' WS-STEP-3-AMT
           DISPLAY 'Plan Pays:       ' WS-PLAN-PAYS
           DISPLAY 'Member Pays:     ' WS-MEMBER-PAYS
           DISPLAY 'Verify Total:    ' WS-VERIFY-TOTAL.

The verification at the end checks that Plan-Pays + Member-Pays + Writeoff = Billed-Charge. This identity must hold exactly for every claim. Sarah Kim reviews this verification in every claim payment program: "If the verification fails, we do not pay the claim. Period. We investigate."


5.21 Chapter Summary

  • Explicit arithmetic verbs (ADD, SUBTRACT, MULTIPLY, DIVIDE) are clearest for simple operations. DIVIDE with REMAINDER is essential for penny-exact distribution.
  • COMPUTE handles complex algebraic expressions. Use it for formulas with more than two operands or exponentiation.
  • ROUNDED prevents silent truncation. Use it on the final result. Carry extra precision in intermediates and round only once.
  • ON SIZE ERROR catches overflow and division by zero. Every DIVIDE and every COMPUTE involving multiplication should have it.
  • Intermediate result precision follows compiler-specific rules. Understand your compiler's limits (18 vs. 31 digits) and restructure expressions if needed.
  • COMP-3 is exact for decimal arithmetic. Floating point (COMP-1/COMP-2) introduces representation errors that accumulate. Never use floating point for money.
  • Financial patterns — penny distribution, compound interest, loan amortization, claim proration — all require careful attention to precision, rounding, and verification.

The overarching theme is Defensive Programming: assume that any arithmetic operation can overflow, any division can divide by zero, and any rounding can lose a penny. Build your programs to detect and handle these conditions explicitly. As Maria Chen says: "Trust your arithmetic, but verify your results."

In Chapter 6, we will explore advanced conditional logic — EVALUATE statements, nested IF structures, condition-name logic, and the patterns that make complex business rules readable and maintainable.


Key Terms Introduced

Term Definition
COMPUTE COBOL statement that evaluates an algebraic expression
ROUNDED Phrase that requests rounding instead of truncation
ON SIZE ERROR Phrase that handles arithmetic overflow and division by zero
REMAINDER DIVIDE clause that captures the leftover after integer division
Intermediate result Temporary value generated during multi-step arithmetic evaluation
Packed decimal arithmetic Exact decimal arithmetic using COMP-3 representation
Floating-point error Representation error from storing decimal values in binary floating-point format
Penny distribution Pattern for dividing a monetary total so every cent is accounted for
Banker's rounding NEAREST-EVEN rounding that eliminates the upward bias of "round half up"
ARITH(EXTEND) IBM compiler option enabling 31-digit intermediate results
Proration Distributing a total across categories according to percentages or ratios
Compound interest Interest calculated on the initial principal plus accumulated interest
Amortization The process of paying off a debt over time through regular payments