> "In COBOL, a penny is a penny. In floating point, a penny is 0.009999999776482582..." — anonymous mainframe programmer
In This Chapter
- 5.1 The Two Families of COBOL Arithmetic
- 5.2 The Explicit Arithmetic Verbs in Depth
- 5.3 The COMPUTE Statement in Depth
- 5.4 The ROUNDED Phrase
- 5.5 ON SIZE ERROR: Overflow Handling
- 5.6 Intermediate Result Precision
- 5.7 Decimal vs. Binary vs. Floating Point
- 5.8 Rounding Error Analysis
- 5.9 GlobalBank Case Study: The BAL-CALC Program
- 5.10 Financial Calculation Patterns
- 5.11 MedClaim Case Study: Claim Proration
- 5.12 Common Arithmetic Bugs and How to Avoid Them
- 5.13 Arithmetic Performance Considerations
- 5.14 Worked Example: Complete Interest Calculation Program
- 5.15 COBOL 2002+ Arithmetic Enhancements
- 5.16 The Penny Rounding Problem in Detail
- 5.17 Currency Calculations and Tax Computation Patterns
- 5.18 Worked Example: Complete Payroll Tax Withholding
- 5.19 The ARITH Compiler Option: Practical Implications
- 5.20 Worked Example: Multi-Step Claim Payment with Full Audit Trail
- 5.21 Chapter Summary
- Key Terms Introduced
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:
- Explicit arithmetic verbs: ADD, SUBTRACT, MULTIPLY, DIVIDE — individual statements for each operation
- 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:
- The conversion from decimal to binary and back incurs a small CPU cost
- 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:
- WS-DAILY-RATE has 9 decimal places — far more than needed for the final result, but essential for intermediate precision
- WS-DAILY-INTEREST and WS-ACCRUED-INT have 6 decimal places — carrying extra precision through the accumulation loop
- WS-POSTED-INT has only 2 decimal places with ROUNDED — rounding occurs only at the very end
- WS-ROUNDING-ADJ tracks the rounding difference — essential for audit trails
- 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
- Modify the program to read account data from a sequential file instead of hard-coded values
- Add a report that shows the daily interest for each day of the month
- Implement the penny distribution pattern for a case where interest must be split between a checking and savings account in a 60/40 ratio
- 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:
- List all COMPUTE statements with multiplication
- For each, sum the integer digits and decimal digits of all operands
- If the total exceeds 18, flag for ARITH(EXTEND)
- 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 |