Every programming language can add two numbers. What sets COBOL apart is how it adds them---and what happens to the pennies.
In This Chapter
- Introduction: Why COBOL Arithmetic Is Different
- The Five Arithmetic Verbs
- The ADD Statement
- The SUBTRACT Statement
- The MULTIPLY Statement
- The DIVIDE Statement
- The COMPUTE Statement
- The ROUNDED Phrase
- ON SIZE ERROR and NOT ON SIZE ERROR
- The GIVING Phrase and Operand Preservation
- Decimal Alignment
- Truncation Rules
- Intermediate Results in COMPUTE
- USAGE COMP-3: Packed Decimal for Financial Calculations
- Arithmetic with Signed Numbers
- Financial Calculation Patterns
- Rounding Rules for Financial Applications
- Common Mistakes and How to Avoid Them
- Complete Example: Financial Report Calculator
- Summary
- Code Examples for This Chapter
Chapter 6: Arithmetic Operations and Numeric Processing
Introduction: Why COBOL Arithmetic Is Different
Every programming language can add two numbers. What sets COBOL apart is how it adds them---and what happens to the pennies.
Consider a simple calculation: divide $10.00 among three people. In Python, JavaScript, or Java using standard floating-point arithmetic, 10.00 / 3 produces 3.3333333333333335---a result that cannot be exactly represented in binary floating-point. Multiply that back by three, and you get 10.000000000000002, not 10.00. In isolation, this seems trivial. Across millions of daily banking transactions, those tiny errors accumulate into real money that must be accounted for.
COBOL was designed from the ground up for business arithmetic. Its decimal data types store numbers exactly as humans write them---0.10 is stored as precisely 0.10, not as the binary approximation 0.1000000000000000055511151231257827021181583404541015625. This is not an academic distinction. It is the reason that more than 70% of the world's financial transaction processing still runs on COBOL, and it is the reason regulatory bodies in banking, insurance, and government trust COBOL systems with trillions of dollars daily.
This chapter teaches you COBOL's five arithmetic verbs---ADD, SUBTRACT, MULTIPLY, DIVIDE, and COMPUTE---along with the critical supporting features that make financial calculations reliable: the ROUNDED phrase, ON SIZE ERROR handling, decimal alignment, and the COMP-3 packed decimal format. By the end, you will be able to write arithmetic code that handles real-world financial calculations with the precision that business demands.
The Five Arithmetic Verbs
COBOL provides five dedicated verbs for arithmetic. The first four---ADD, SUBTRACT, MULTIPLY, and DIVIDE---use an English-like syntax that reads almost like a business instruction. The fifth, COMPUTE, provides a more conventional algebraic notation for complex expressions.
Each verb supports: - The ROUNDED phrase, which controls rounding behavior - The ON SIZE ERROR phrase, which detects overflow - The NOT ON SIZE ERROR phrase, which executes on success - The GIVING phrase (except COMPUTE, where assignment is implicit), which preserves the original operands
A Note on Format
All code examples in this chapter use COBOL fixed-format (columns 1-6 for sequence numbers, column 7 for indicator, columns 8-11 for Area A, columns 12-72 for Area B). Free-format equivalents are syntactically identical but without column restrictions. Where instructive, we will note format differences.
The ADD Statement
The ADD statement has three primary forms, each serving different needs.
Form 1: ADD a TO b
ADD WS-TAX TO WS-TOTAL
This is the most direct form. The value of WS-TAX is added to WS-TOTAL, and the result replaces the value in WS-TOTAL. The value of WS-TAX remains unchanged.
Semantics: WS-TOTAL = WS-TOTAL + WS-TAX
You may add a literal value:
ADD 100 TO WS-BALANCE
You may also add to multiple targets simultaneously:
ADD WS-AMOUNT TO WS-SUBTOTAL WS-GRAND-TOTAL
This single statement adds WS-AMOUNT to both WS-SUBTOTAL and WS-GRAND-TOTAL independently. It is equivalent to writing two separate ADD statements.
Form 2: ADD a b GIVING c
ADD WS-PRICE WS-TAX GIVING WS-TOTAL
This form adds the values of all identifiers and literals before the word GIVING, then stores the sum in the identifier(s) after GIVING. All operands before GIVING remain unchanged.
Semantics: WS-TOTAL = WS-PRICE + WS-TAX
The GIVING form is valuable because it preserves both source operands. In financial systems, you often need the original values for audit trails, reports, or subsequent calculations.
You can sum multiple values:
ADD WS-ITEM-1 WS-ITEM-2 WS-ITEM-3 WS-SHIPPING
GIVING WS-ORDER-TOTAL
And you can store results in multiple targets:
ADD WS-AMOUNT-1 WS-AMOUNT-2
GIVING WS-REPORT-TOTAL WS-AUDIT-TOTAL
Form 3: ADD CORRESPONDING
ADD CORRESPONDING WS-MONTHLY-BUDGET
TO WS-ANNUAL-TOTALS
ADD CORRESPONDING (abbreviated ADD CORR) matches elementary items within two group items by name. If WS-MONTHLY-BUDGET contains fields named WS-RENT, WS-UTILITIES, and WS-GROCERIES, and WS-ANNUAL-TOTALS contains identically named fields, then each matching pair is added.
This is particularly useful for accumulating period-over-period totals. Consider a payroll system that accumulates monthly figures into yearly totals---ADD CORRESPONDING handles the entire group in a single statement.
Requirements for CORRESPONDING: - Both operands must be group items - Elementary items are matched by name (qualified names must match) - Only numeric items are processed - Non-matching items are ignored
Example with full context:
01 WS-MONTHLY-BUDGET.
05 WS-RENT PIC 9(5)V99 VALUE 1500.00.
05 WS-UTILITIES PIC 9(5)V99 VALUE 200.00.
05 WS-GROCERIES PIC 9(5)V99 VALUE 400.00.
01 WS-ANNUAL-TOTALS.
05 WS-RENT PIC 9(7)V99 VALUE ZERO.
05 WS-UTILITIES PIC 9(7)V99 VALUE ZERO.
05 WS-GROCERIES PIC 9(7)V99 VALUE ZERO.
After ADD CORR WS-MONTHLY-BUDGET TO WS-ANNUAL-TOTALS, each field in WS-ANNUAL-TOTALS has the corresponding monthly value added to it.
The SUBTRACT Statement
SUBTRACT mirrors ADD with its own three forms.
Form 1: SUBTRACT a FROM b
SUBTRACT WS-DISCOUNT FROM WS-TOTAL
Semantics: WS-TOTAL = WS-TOTAL - WS-DISCOUNT
The value of WS-DISCOUNT is subtracted from WS-TOTAL, and the result replaces WS-TOTAL. The value of WS-DISCOUNT is unchanged.
You can subtract from multiple targets:
SUBTRACT WS-FEE FROM WS-CHECKING WS-SAVINGS
Form 2: SUBTRACT a FROM b GIVING c
SUBTRACT WS-RETURNS FROM WS-GROSS-SALES
GIVING WS-NET-SALES
Semantics: WS-NET-SALES = WS-GROSS-SALES - WS-RETURNS
Both WS-RETURNS and WS-GROSS-SALES remain unchanged. This is essential when both the gross and return figures are needed later.
You can subtract multiple values:
SUBTRACT WS-TAX WS-INSURANCE WS-401K
FROM WS-GROSS-PAY
GIVING WS-NET-PAY
Semantics: WS-NET-PAY = WS-GROSS-PAY - WS-TAX - WS-INSURANCE - WS-401K
Form 3: SUBTRACT CORRESPONDING
SUBTRACT CORRESPONDING WS-ADJUSTMENTS
FROM WS-ACCOUNT-BALANCES
This works identically to ADD CORRESPONDING but subtracts matching elementary items instead of adding them.
The MULTIPLY Statement
MULTIPLY has two forms.
Form 1: MULTIPLY a BY b
MULTIPLY WS-RATE BY WS-AMOUNT
Semantics: WS-AMOUNT = WS-AMOUNT * WS-RATE
The result replaces WS-AMOUNT. The value of WS-RATE is unchanged.
Important: Notice the English reading: "multiply the rate by the amount." The result goes into the identifier after BY, not before it. This is a common point of confusion for beginners.
Form 2: MULTIPLY a BY b GIVING c
MULTIPLY WS-PRICE BY WS-QUANTITY
GIVING WS-LINE-TOTAL
Semantics: WS-LINE-TOTAL = WS-PRICE * WS-QUANTITY
Both WS-PRICE and WS-QUANTITY are preserved. The result field WS-LINE-TOTAL should be large enough to hold the product.
Sizing rule for multiplication: If you multiply a PIC 9(m)V9(p) by a PIC 9(n)V9(q), the full-precision result needs PIC 9(m+n)V9(p+q). For example, multiplying PIC 9(5)V99 by PIC 9(3)V99 could produce up to 8 integer digits and 4 decimal places.
The DIVIDE Statement
DIVIDE is the most syntactically complex arithmetic verb, with four forms and the unique REMAINDER phrase.
Form 1: DIVIDE a INTO b
DIVIDE WS-NUM-EMPLOYEES INTO WS-TOTAL-BONUS
Semantics: WS-TOTAL-BONUS = WS-TOTAL-BONUS / WS-NUM-EMPLOYEES
Read it as: "divide the number of employees into the total bonus." The result replaces the identifier after INTO.
Form 2: DIVIDE a INTO b GIVING c
DIVIDE WS-NUM-SHARES INTO WS-TOTAL-DIVIDEND
GIVING WS-PER-SHARE-AMOUNT
Semantics: WS-PER-SHARE-AMOUNT = WS-TOTAL-DIVIDEND / WS-NUM-SHARES
Both operands are preserved.
Form 3: DIVIDE a BY b GIVING c
DIVIDE WS-TOTAL-SALES BY WS-NUM-MONTHS
GIVING WS-MONTHLY-AVERAGE
Semantics: WS-MONTHLY-AVERAGE = WS-TOTAL-SALES / WS-NUM-MONTHS
The BY form reads more naturally for many programmers. "Divide total sales by number of months" maps directly to the mathematical expression.
Form 4: DIVIDE with REMAINDER
DIVIDE WS-TOTAL-AMOUNT BY WS-NUM-PEOPLE
GIVING WS-EACH-SHARE
REMAINDER WS-LEFTOVER
The REMAINDER phrase is only available with GIVING. It provides the integer quotient in the GIVING field and the remainder separately. The remainder is calculated as:
REMAINDER = DIVIDEND - (QUOTIENT * DIVISOR)
Practical example---splitting a bill:
MOVE 100.00 TO WS-BILL
MOVE 3 TO WS-PEOPLE
DIVIDE WS-BILL BY WS-PEOPLE
GIVING WS-EACH-SHARE
REMAINDER WS-LEFTOVER
After execution: WS-EACH-SHARE = 33 (integer quotient), WS-LEFTOVER = 1. For currency amounts, you would typically use ROUNDED without REMAINDER, or handle the remainder as cents to distribute.
INTO vs BY: Clarifying the Confusion
The difference between INTO and BY is the position of the dividend and divisor:
| Statement | Meaning | Formula |
|---|---|---|
DIVIDE A INTO B |
Divide B by A | B = B / A |
DIVIDE A BY B GIVING C |
Divide A by B | C = A / B |
DIVIDE A INTO B GIVING C |
Divide B by A | C = B / A |
The mnemonic: INTO means "divide this number into that quantity"---the second operand is the one being divided. BY means "divide this number by that number"---standard mathematical order.
The COMPUTE Statement
COMPUTE brings conventional algebraic notation to COBOL. Instead of separate verbs, you write expressions using arithmetic operators.
Basic Syntax
COMPUTE WS-RESULT = WS-A + WS-B * WS-C
Operators
| Operator | Operation | Example |
|---|---|---|
+ |
Addition | A + B |
- |
Subtraction | A - B |
* |
Multiplication | A * B |
/ |
Division | A / B |
** |
Exponentiation | A ** B (A raised to power B) |
Operator Precedence
COBOL follows standard mathematical precedence:
- Parentheses (innermost first)
**Exponentiation (highest)*/Multiplication and Division (left to right)+-Addition and Subtraction (left to right)
Example demonstrating precedence:
COMPUTE WS-RESULT = 2 + 3 * 4
Result: 14 (not 20), because multiplication is performed before addition: 2 + (3 * 4) = 2 + 12 = 14.
COMPUTE WS-RESULT = (2 + 3) * 4
Result: 20, because parentheses override precedence: (2 + 3) * 4 = 5 * 4 = 20.
Full precedence chain:
COMPUTE WS-RESULT = 10 + 2 ** 3 * 4 - 8 / 2
Evaluation order:
1. 2 ** 3 = 8 (exponentiation first)
2. 8 * 4 = 32 and 8 / 2 = 4 (multiplication/division, left to right)
3. 10 + 32 - 4 = 38 (addition/subtraction, left to right)
Exponentiation
The ** operator provides exponentiation, which has no equivalent among the other four arithmetic verbs:
COMPUTE WS-SQUARE = WS-VALUE ** 2
COMPUTE WS-CUBE = WS-VALUE ** 3
COMPUTE WS-SQRT = WS-VALUE ** 0.5
Fractional exponents enable root calculations. X ** 0.5 computes the square root; X ** (1.0 / 3.0) computes the cube root.
When to Use COMPUTE vs Individual Verbs
Use COMPUTE when: - The expression involves multiple operations - You need exponentiation - The formula is easier to read in algebraic notation - You are implementing a mathematical formula (compound interest, amortization, etc.)
Use individual verbs (ADD, SUBTRACT, etc.) when: - The operation is simple and benefits from self-documenting syntax - You want maximum clarity for maintenance programmers - You need ADD CORRESPONDING or SUBTRACT CORRESPONDING - You need DIVIDE with REMAINDER
Example---compound interest formula:
Using individual verbs (awkward):
ADD 1 TO WS-MONTHLY-RATE GIVING WS-TEMP
COMPUTE WS-POWER = WS-TEMP ** WS-NUM-PERIODS
MULTIPLY WS-MONTHLY-RATE BY WS-POWER GIVING WS-NUMER
SUBTRACT 1 FROM WS-POWER GIVING WS-DENOM
DIVIDE WS-NUMER BY WS-DENOM GIVING WS-FACTOR
MULTIPLY WS-PRINCIPAL BY WS-FACTOR
GIVING WS-PAYMENT ROUNDED
Using COMPUTE (clear and direct):
COMPUTE WS-PAYMENT ROUNDED =
WS-PRINCIPAL *
(WS-MONTHLY-RATE *
(1 + WS-MONTHLY-RATE) ** WS-NUM-PERIODS) /
((1 + WS-MONTHLY-RATE) ** WS-NUM-PERIODS - 1)
The COMPUTE version maps directly to the mathematical formula M = P * [r(1+r)^n] / [(1+r)^n - 1], making it easier to verify against the specification.
The ROUNDED Phrase
By default, when an arithmetic result has more decimal places than the receiving field, COBOL truncates---it simply drops the excess digits. The ROUNDED phrase changes this behavior to perform standard rounding.
Default Behavior (Truncation)
01 WS-RESULT PIC 9(5)V99.
COMPUTE WS-RESULT = 10 / 3
The true result is 3.333333... The PIC 9(5)V99 field can hold two decimal places. Without ROUNDED, the result is 3.33---the digits beyond the second decimal place are discarded.
With ROUNDED
COMPUTE WS-RESULT ROUNDED = 10 / 3
Result: 3.33 (because the truncated digit is 3, which is less than 5, rounding does not change the result in this case).
COMPUTE WS-RESULT ROUNDED = 2 / 3
True result: 0.666666... Truncated: 0.66. Rounded: 0.67 (because the first truncated digit is 6, which is >= 5).
Where ROUNDED Appears
The ROUNDED phrase immediately follows the result identifier in any arithmetic verb:
ADD WS-A TO WS-B ROUNDED
SUBTRACT WS-A FROM WS-B GIVING WS-C ROUNDED
MULTIPLY WS-A BY WS-B GIVING WS-C ROUNDED
DIVIDE WS-A BY WS-B GIVING WS-C ROUNDED
COMPUTE WS-RESULT ROUNDED = WS-A / WS-B
ROUNDED MODE (COBOL 2002 and Later)
The COBOL 2002 standard introduced ROUNDED MODE, which provides explicit control over the rounding algorithm. The syntax is:
COMPUTE WS-RESULT ROUNDED MODE IS NEAREST-EVEN
= WS-A / WS-B
Available modes include:
| Mode | Behavior | Example: 2.5 rounds to |
|---|---|---|
| AWAY-FROM-ZERO | Round away from zero | 3 |
| NEAREST-AWAY-FROM-ZERO | Standard rounding (default) | 3 |
| NEAREST-EVEN | Banker's rounding | 2 |
| NEAREST-TOWARD-ZERO | Round toward zero when exactly half | 2 |
| TOWARD-GREATER | Always round toward positive infinity | 3 |
| TOWARD-LESSER | Always round toward negative infinity | 2 |
| TRUNCATION | Truncate (no rounding) | 2 |
Banker's Rounding (NEAREST-EVEN) is especially important for financial applications. When the truncated portion is exactly 5 (i.e., the value is exactly halfway), standard rounding always rounds up, introducing a systematic upward bias. Banker's rounding instead rounds to the nearest even digit, eliminating this bias over large numbers of transactions.
Example: - 2.5 rounds to 2 (nearest even) - 3.5 rounds to 4 (nearest even) - 4.5 rounds to 4 (nearest even) - 5.5 rounds to 6 (nearest even)
Over thousands of transactions, half-up rounding accumulates an average error of +0.5 cents per rounding event. Banker's rounding averages to zero error because it rounds up and down equally often at the halfway point.
Note
ROUNDED MODE support varies by compiler. GnuCOBOL supports most modes as of version 3.x. IBM Enterprise COBOL has supported ROUNDED MODE since V4.2. Always check your compiler documentation.
ON SIZE ERROR and NOT ON SIZE ERROR
What Is a Size Error?
A size error occurs when the result of an arithmetic operation exceeds the capacity of the receiving field. This includes:
-
Overflow: The integer portion of the result has more digits than the receiving field allows. - Example: Storing 1200 in a PIC 9(3) field (which holds 0-999).
-
Division by zero: Any division where the divisor is zero.
-
Unsigned negative: Subtracting a larger number from a smaller one when the result field is unsigned (no S in PIC).
Without ON SIZE ERROR
If a size error occurs without the ON SIZE ERROR phrase, the result is undefined. The receiving field may contain truncated data, zeros, or garbage. Critically, no error is raised---the program continues silently with bad data. This is one of the most dangerous behaviors in COBOL and a leading cause of subtle financial bugs.
With ON SIZE ERROR
ADD WS-AMOUNT TO WS-TOTAL
ON SIZE ERROR
DISPLAY "Overflow in total accumulation!"
MOVE "Y" TO WS-ERROR-FLAG
NOT ON SIZE ERROR
CONTINUE
END-ADD
When ON SIZE ERROR is specified:
- If overflow occurs, the receiving field is not modified and the SIZE ERROR imperative executes.
- If no overflow occurs, the NOT ON SIZE ERROR imperative executes (if present).
- The END-ADD (or corresponding END-verb) scope terminator is required when using SIZE ERROR.
Scope Terminators
Every arithmetic verb has a corresponding scope terminator when used with SIZE ERROR:
ADD ... END-ADD
SUBTRACT ... END-SUBTRACT
MULTIPLY ... END-MULTIPLY
DIVIDE ... END-DIVIDE
COMPUTE ... END-COMPUTE
Division by Zero
Division by zero always triggers ON SIZE ERROR. Without it, the behavior is undefined:
DIVIDE WS-TOTAL BY WS-COUNT
GIVING WS-AVERAGE ROUNDED
ON SIZE ERROR
DISPLAY "Cannot compute average: zero count"
MOVE ZERO TO WS-AVERAGE
END-DIVIDE
Best practice: Always check the divisor before dividing, even with ON SIZE ERROR:
IF WS-COUNT > ZERO
DIVIDE WS-TOTAL BY WS-COUNT
GIVING WS-AVERAGE ROUNDED
ON SIZE ERROR
PERFORM 9999-HANDLE-OVERFLOW
END-DIVIDE
ELSE
MOVE ZERO TO WS-AVERAGE
END-IF
This two-layer defense prevents both division by zero and overflow, while providing clear error handling for each case.
Why ON SIZE ERROR Matters for Financial Accuracy
In a financial system processing millions of transactions, a single undetected overflow can: - Produce incorrect account balances - Generate wrong tax calculations - Cause regulatory violations - Create audit discrepancies that are extremely difficult to trace
The cost of adding ON SIZE ERROR to every arithmetic operation is trivial. The cost of an undetected overflow in production can be enormous. Use ON SIZE ERROR on every financial calculation without exception.
The GIVING Phrase and Operand Preservation
The GIVING phrase appears in ADD, SUBTRACT, MULTIPLY, and DIVIDE (not COMPUTE, where the target is always specified with =). Its primary purpose is to preserve the original operands.
Why Preservation Matters
Consider an invoice calculation:
* WITHOUT GIVING - WS-QUANTITY is destroyed
MULTIPLY WS-PRICE BY WS-QUANTITY
* WS-QUANTITY now contains the line total, not the quantity!
* You cannot print quantity on the invoice anymore.
* WITH GIVING - both operands preserved
MULTIPLY WS-PRICE BY WS-QUANTITY
GIVING WS-LINE-TOTAL
* WS-PRICE and WS-QUANTITY are unchanged for reporting.
In financial systems, original transaction values must be preserved for: - Audit trails - Report generation - Subsequent calculations - Regulatory compliance - Reprocessing and reconciliation
Rule of thumb: Use GIVING whenever you need the original values after the arithmetic. Use the "TO/FROM/BY" form only when you intentionally want to replace the operand (such as accumulating a running total).
Decimal Alignment
One of COBOL's most powerful features for business arithmetic is automatic decimal alignment. When you perform arithmetic on fields with different decimal positions, COBOL automatically aligns the decimal points before operating.
How It Works
01 WS-PRICE PIC 9(3)V9 VALUE 123.4.
01 WS-TAX-RATE PIC V9(4) VALUE 0.0875.
01 WS-RESULT PIC 9(5)V9(4) VALUE ZERO.
MULTIPLY WS-PRICE BY WS-TAX-RATE
GIVING WS-RESULT
COBOL treats WS-PRICE as 123.4000 and WS-TAX-RATE as 0.0875 for the computation. The result 10.7975 is stored with four decimal places in WS-RESULT.
Alignment During Assignment
When the result is stored, COBOL aligns the decimal point of the result with the decimal point of the receiving field:
- If the receiving field has fewer decimal places, excess places are truncated (or rounded with ROUNDED).
- If the receiving field has more decimal places, the extras are filled with zeros.
- If the receiving field has fewer integer positions and the result exceeds them, a size error occurs.
Example:
01 WS-WIDE PIC 9(5)V9(4) VALUE ZERO.
01 WS-NARROW PIC 9(3)V99 VALUE ZERO.
COMPUTE WS-WIDE = 1 / 3
* WS-WIDE = 00000.3333
COMPUTE WS-NARROW = 1 / 3
* WS-NARROW = 000.33 (truncated)
COMPUTE WS-NARROW ROUNDED = 1 / 3
* WS-NARROW = 000.33 (rounded, same in this case)
Truncation Rules
Understanding when and how truncation occurs is essential for writing correct financial code.
When Truncation Happens
-
Decimal truncation: When the result has more decimal places than the receiving field. -
1 / 3 = 0.333...stored in PIC V99 becomes0.33 -
Integer truncation: When the result has more integer digits than the receiving field (this is a size error). -
999 + 1 = 1000stored in PIC 999 causes a size error -
Intermediate result truncation: When intermediate calculations in a multi-step process lose precision.
Preventing Unwanted Truncation
-
Use ROUNDED for all currency calculations:
cobol MULTIPLY WS-AMOUNT BY WS-RATE GIVING WS-TAX ROUNDED -
Size receiving fields appropriately: - For addition/subtraction: the result needs one more integer digit than the larger operand - For multiplication: the result needs the sum of the operands' integer digits and decimal places - For division: the result may need many decimal places
-
Use ON SIZE ERROR to detect integer overflow:
cobol ADD WS-AMOUNT TO WS-TOTAL ON SIZE ERROR PERFORM 9999-OVERFLOW-HANDLER END-ADD -
Multiply before dividing to reduce intermediate precision loss: ```cobol
-
Less precise: dividing first creates a repeating decimal COMPUTE WS-RESULT = WS-A / WS-B * WS-C
-
More precise: multiplying first avoids the repeating decimal COMPUTE WS-RESULT = WS-A * WS-C / WS-B ```
-
Intermediate Results in COMPUTE
When COBOL evaluates a COMPUTE expression, the compiler generates code to hold intermediate results. The precision of these intermediate results can affect the final answer.
Compiler Behavior
Most COBOL compilers use an internal precision for intermediate results that is sufficient to avoid losing significant digits. The COBOL standard specifies that intermediate results should have at least enough decimal places to ensure the final result is as precise as the receiving field allows.
However, the exact rules vary by compiler:
- IBM Enterprise COBOL: Uses a well-documented algorithm for intermediate result sizing. The ARITH(EXTEND) compiler option provides up to 31-digit intermediate precision.
- GnuCOBOL: Typically uses sufficient precision for intermediate calculations, with behavior controlled by compiler settings.
- Micro Focus COBOL: Provides compiler directives to control intermediate precision.
Practical Impact
Consider this expression:
COMPUTE WS-RESULT = 100 / 3 * 3
Mathematically, this should equal 100. But if the intermediate result of 100 / 3 is stored with limited precision (say, as 33.33), then 33.33 * 3 = 99.99, not 100.
With COMPUTE, most modern compilers maintain enough intermediate precision to produce 100.00. However, if you break this into separate statements:
DIVIDE 3 INTO 100 GIVING WS-TEMP
MULTIPLY 3 BY WS-TEMP GIVING WS-RESULT
The intermediate result is limited by the PIC clause of WS-TEMP. If WS-TEMP is PIC 9(5)V99, the intermediate is 33.33, and the final result is 99.99.
Guideline: Use COMPUTE for complex expressions to benefit from the compiler's intermediate precision management. Reserve individual verbs for simple, single-operation calculations.
USAGE COMP-3: Packed Decimal for Financial Calculations
COMP-3 (packed decimal) is the workhorse data type for financial calculations on mainframe systems. Understanding why it matters is essential for any COBOL programmer working with money.
How COMP-3 Works
COMP-3 stores two decimal digits per byte, with the last byte's low nibble reserved for the sign:
Value: +12345
Storage: 01 23 45 0C (four bytes)
^ ^ ^ ^^
| | | |+-- C = positive sign
| | | +--- 5 (ones digit)
| | +------ 4,3 (tens, hundreds)
| +--------- 2,1 (thousands, ten-thousands)
+------------ 0 (leading zero for even digit count)
Sign nibbles: C = positive, D = negative, F = unsigned.
Storage formula: A PIC 9(n) COMP-3 field requires CEIL((n + 1) / 2) bytes.
| PIC Clause | Digits | COMP-3 Bytes | DISPLAY Bytes |
|---|---|---|---|
| 9(3) | 3 | 2 | 3 |
| 9(5)V99 | 7 | 4 | 7 |
| 9(9)V99 | 11 | 6 | 11 |
| 9(15)V99 | 17 | 9 | 17 |
| 9(18) | 18 | 10 | 18 |
Why COMP-3 for Financial Data
-
Exact decimal representation: Unlike binary (COMP) or floating-point (COMP-1/COMP-2), COMP-3 stores decimal values exactly. The value
0.10is stored as precisely0.10, not as a binary approximation. -
Space efficiency: COMP-3 uses roughly half the storage of DISPLAY format. For files with millions of records, this reduces I/O and storage costs significantly.
-
Hardware acceleration: IBM mainframes have native hardware instructions for packed decimal arithmetic (AP, SP, MP, DP, ZAP). These execute in a single machine cycle, making COMP-3 arithmetic extremely fast on mainframe hardware.
-
Industry standard: Most mainframe databases (DB2, IMS, VSAM) store monetary values in packed decimal format. Using COMP-3 in your programs avoids conversion overhead.
Declaring COMP-3 Fields
01 WS-AMOUNT PIC S9(9)V99 COMP-3 VALUE ZERO.
01 WS-RATE PIC SV9(6) COMP-3 VALUE ZERO.
01 WS-TOTAL PIC S9(11)V99 COMP-3 VALUE ZERO.
Best practices for COMP-3 declarations:
- Always include the S (signed) in the PIC clause for arithmetic fields
- Use an odd total digit count for optimal alignment (COMP-3 always uses (digits + 1) / 2 bytes; an odd digit count wastes no space)
- Size the field to accommodate the largest expected value plus overflow margin
Performance Considerations: COMP-3 vs DISPLAY vs COMP
| Usage | Storage | Arithmetic Speed | Best For |
|---|---|---|---|
| DISPLAY | 1 byte/digit | Slowest (requires conversion) | Report output, small files |
| COMP-3 | ~0.5 bytes/digit | Fast (native on mainframes) | Financial data, database fields |
| COMP | 2, 4, or 8 bytes | Fastest for integer math | Counters, subscripts, loop vars |
| COMP-1 | 4 bytes | Fast (hardware float) | Scientific, NEVER for money |
| COMP-2 | 8 bytes | Fast (hardware float) | Scientific, NEVER for money |
General guidance: - Use COMP-3 for all monetary amounts and financial calculations - Use COMP for counters, subscripts, and loop variables - Use DISPLAY for fields that will be directly output or are part of flat-file records - Never use COMP-1 or COMP-2 for financial calculations
Arithmetic with Signed Numbers
Declaring Signed Fields
To allow negative values, include the S in the PIC clause:
01 WS-BALANCE PIC S9(7)V99 VALUE ZERO.
01 WS-ADJUSTMENT PIC S9(5)V99 VALUE -250.00.
Without the S, the field is unsigned and can only hold zero or positive values. Attempting to store a negative result in an unsigned field causes a size error.
Signed Arithmetic Behavior
MOVE +300.00 TO WS-A
MOVE -150.50 TO WS-B
ADD WS-A WS-B GIVING WS-RESULT
* Result: +149.50
SUBTRACT WS-B FROM WS-A GIVING WS-RESULT
* Result: +450.50 (subtracting a negative adds)
Key rules: - Adding a negative number subtracts - Subtracting a negative number adds - The sign of the result follows standard mathematical rules - An unsigned field receiving a negative result triggers a size error (with ON SIZE ERROR) or produces undefined results (without it)
Sign Storage
The sign is stored differently depending on USAGE: - DISPLAY: In the zone portion of the last byte (or with SIGN LEADING/TRAILING SEPARATE) - COMP-3: In the low nibble of the last byte (C=positive, D=negative) - COMP: As the binary sign bit (two's complement)
Financial Calculation Patterns
Simple Interest
Formula: I = P * R * T
Where: - I = interest earned - P = principal (initial amount) - R = annual interest rate (as decimal) - T = time in years
COMPUTE WS-INTEREST ROUNDED =
WS-PRINCIPAL * WS-RATE * WS-YEARS
COMPUTE WS-TOTAL =
WS-PRINCIPAL + WS-INTEREST
Compound Interest
Formula: FV = P * (1 + r/n)^(n*t)
Where: - FV = future value - P = principal - r = annual interest rate (decimal) - n = compounding periods per year - t = number of years
COMPUTE WS-FUTURE-VALUE ROUNDED =
WS-PRINCIPAL *
(1 + WS-RATE / WS-COMPOUNDS-PER-YEAR)
** (WS-COMPOUNDS-PER-YEAR * WS-YEARS)
Monthly Loan Payment
Formula: M = P * [r(1+r)^n] / [(1+r)^n - 1]
Where: - M = monthly payment - P = principal (loan amount) - r = monthly interest rate (annual rate / 12) - n = total number of payments
COMPUTE WS-MONTHLY-RATE = WS-ANNUAL-RATE / 12
COMPUTE WS-NUM-PAYMENTS = WS-TERM-YEARS * 12
COMPUTE WS-MONTHLY-PAYMENT ROUNDED =
WS-PRINCIPAL *
(WS-MONTHLY-RATE *
(1 + WS-MONTHLY-RATE) ** WS-NUM-PAYMENTS) /
((1 + WS-MONTHLY-RATE) ** WS-NUM-PAYMENTS - 1)
ON SIZE ERROR
DISPLAY "Payment calculation overflow!"
END-COMPUTE
Progressive Tax Brackets
Tax bracket calculations require processing each bracket sequentially:
MOVE WS-TAXABLE-INCOME TO WS-REMAINING
MOVE ZERO TO WS-TOTAL-TAX
IF WS-REMAINING > 11000
COMPUTE WS-BRACKET-TAX = 11000 * 0.10
SUBTRACT 11000 FROM WS-REMAINING
ELSE
COMPUTE WS-BRACKET-TAX = WS-REMAINING * 0.10
MOVE ZERO TO WS-REMAINING
END-IF
ADD WS-BRACKET-TAX TO WS-TOTAL-TAX
IF WS-REMAINING > 33725
COMPUTE WS-BRACKET-TAX = 33725 * 0.12
SUBTRACT 33725 FROM WS-REMAINING
ELSE
COMPUTE WS-BRACKET-TAX ROUNDED =
WS-REMAINING * 0.12
MOVE ZERO TO WS-REMAINING
END-IF
ADD WS-BRACKET-TAX TO WS-TOTAL-TAX
* ... continue for each bracket
Currency Conversion
COMPUTE WS-FOREIGN-AMOUNT ROUNDED =
WS-USD-AMOUNT * WS-EXCHANGE-RATE
ON SIZE ERROR
DISPLAY "Currency conversion overflow!"
END-COMPUTE
For reverse conversion:
IF WS-EXCHANGE-RATE NOT = ZERO
COMPUTE WS-USD-AMOUNT ROUNDED =
WS-FOREIGN-AMOUNT / WS-EXCHANGE-RATE
END-IF
Rounding Rules for Financial Applications
Financial rounding is not optional---it is mandated by regulations, accounting standards, and contractual obligations. Different industries and jurisdictions may require different rounding methods.
Half-Up Rounding (Standard)
This is the default COBOL ROUNDED behavior. When the discarded portion is >= 0.5, round away from zero: - 2.345 rounds to 2.35 - 2.344 rounds to 2.34 - -2.345 rounds to -2.35
Banker's Rounding (Half-Even)
Required by many financial regulations. When the discarded portion is exactly 0.5, round to the nearest even digit: - 2.345 rounds to 2.34 (4 is even) - 2.355 rounds to 2.36 (6 is even) - 2.365 rounds to 2.36 (6 is even) - 2.375 rounds to 2.38 (8 is even)
In COBOL 2002+:
COMPUTE WS-RESULT ROUNDED MODE IS NEAREST-EVEN
= WS-AMOUNT * WS-RATE
Truncation (Floor)
Some jurisdictions require truncation (always round toward zero) for certain calculations, particularly tax withholding: - 2.349 becomes 2.34 - 2.345 becomes 2.34 - 2.341 becomes 2.34
When Rounding Rules Matter
Consider processing 1,000,000 transactions per day, each involving a tax calculation: - Half-up rounding: Average error per transaction = +$0.005. Daily accumulated bias = +$5,000. - Banker's rounding: Average error per transaction = $0.000. Daily accumulated bias = ~$0. - Truncation: Average error per transaction = -$0.005. Daily accumulated bias = -$5,000.
Over a year, the half-up bias amounts to approximately $1.3 million in systematic overcharge. This is why financial regulations often mandate banker's rounding.
Common Mistakes and How to Avoid Them
Mistake 1: Forgetting ON SIZE ERROR
* DANGEROUS - overflow goes undetected
ADD WS-AMOUNT TO WS-TOTAL
* SAFE - overflow is caught
ADD WS-AMOUNT TO WS-TOTAL
ON SIZE ERROR
PERFORM 9999-OVERFLOW-HANDLER
END-ADD
Mistake 2: Dividing Without Checking for Zero
* DANGEROUS - will produce undefined results if COUNT = 0
DIVIDE WS-TOTAL BY WS-COUNT GIVING WS-AVERAGE
* SAFE - checks before dividing
IF WS-COUNT > ZERO
DIVIDE WS-TOTAL BY WS-COUNT
GIVING WS-AVERAGE ROUNDED
ON SIZE ERROR
PERFORM 9999-OVERFLOW-HANDLER
END-DIVIDE
ELSE
MOVE ZERO TO WS-AVERAGE
END-IF
Mistake 3: Undersized Result Fields
* WRONG - product can be up to 10 digits, field holds 7
01 WS-PRICE PIC 9(5)V99.
01 WS-QTY PIC 9(3).
01 WS-TOTAL PIC 9(5)V99. *> Too small!
MULTIPLY WS-PRICE BY WS-QTY
GIVING WS-TOTAL
* RIGHT - result field sized for maximum product
01 WS-TOTAL PIC 9(8)V99. *> Can hold 5+3 integer digits
Mistake 4: Confusing INTO and BY
* "DIVIDE 5 INTO 100" means 100 / 5, not 5 / 100
DIVIDE 5 INTO WS-VALUE
* WS-VALUE = WS-VALUE / 5
* If you want VALUE / DIVISOR stored in RESULT:
DIVIDE WS-VALUE BY WS-DIVISOR
GIVING WS-RESULT
Mistake 5: Using Unsigned Fields for Potentially Negative Results
* WRONG - subtraction could go negative
01 WS-BALANCE PIC 9(7)V99. *> Unsigned!
SUBTRACT WS-WITHDRAWAL FROM WS-BALANCE
* RIGHT - signed field accommodates negatives
01 WS-BALANCE PIC S9(7)V99. *> Signed
SUBTRACT WS-WITHDRAWAL FROM WS-BALANCE
Mistake 6: Losing Precision by Dividing Before Multiplying
* LESS PRECISE
COMPUTE WS-RESULT = WS-A / 3 * WS-B
* MORE PRECISE
COMPUTE WS-RESULT = WS-A * WS-B / 3
Mistake 7: Not Using ROUNDED for Currency
* WRONG - truncation can systematically lose or gain cents
MULTIPLY WS-PRICE BY WS-TAX-RATE
GIVING WS-TAX
* RIGHT - rounding gives the nearest cent
MULTIPLY WS-PRICE BY WS-TAX-RATE
GIVING WS-TAX ROUNDED
Complete Example: Financial Report Calculator
The following example brings together all the concepts in this chapter. It calculates invoice totals with tax, discount, and proper rounding, using COMP-3 fields and full error handling.
IDENTIFICATION DIVISION.
PROGRAM-ID. INVOICE-CALC.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-PRICE PIC S9(5)V99 COMP-3 VALUE ZERO.
01 WS-QTY PIC S9(3) COMP-3 VALUE ZERO.
01 WS-SUBTOTAL PIC S9(7)V99 COMP-3 VALUE ZERO.
01 WS-DISCOUNT-PCT PIC SV9(4) COMP-3 VALUE 0.1500.
01 WS-DISCOUNT-AMT PIC S9(7)V99 COMP-3 VALUE ZERO.
01 WS-AFTER-DISC PIC S9(7)V99 COMP-3 VALUE ZERO.
01 WS-TAX-RATE PIC SV9(4) COMP-3 VALUE 0.0875.
01 WS-TAX-AMT PIC S9(7)V99 COMP-3 VALUE ZERO.
01 WS-GRAND-TOTAL PIC S9(7)V99 COMP-3 VALUE ZERO.
01 WS-ERROR-FLAG PIC 9 VALUE 0.
PROCEDURE DIVISION.
MOVE 49.95 TO WS-PRICE
MOVE 25 TO WS-QTY
MULTIPLY WS-PRICE BY WS-QTY
GIVING WS-SUBTOTAL ROUNDED
ON SIZE ERROR
MOVE 1 TO WS-ERROR-FLAG
END-MULTIPLY
MULTIPLY WS-SUBTOTAL BY WS-DISCOUNT-PCT
GIVING WS-DISCOUNT-AMT ROUNDED
ON SIZE ERROR
MOVE 1 TO WS-ERROR-FLAG
END-MULTIPLY
SUBTRACT WS-DISCOUNT-AMT FROM WS-SUBTOTAL
GIVING WS-AFTER-DISC
ON SIZE ERROR
MOVE 1 TO WS-ERROR-FLAG
END-SUBTRACT
MULTIPLY WS-AFTER-DISC BY WS-TAX-RATE
GIVING WS-TAX-AMT ROUNDED
ON SIZE ERROR
MOVE 1 TO WS-ERROR-FLAG
END-MULTIPLY
ADD WS-AFTER-DISC WS-TAX-AMT
GIVING WS-GRAND-TOTAL
ON SIZE ERROR
MOVE 1 TO WS-ERROR-FLAG
END-ADD
STOP RUN.
This pattern---COMP-3 fields, ROUNDED on every multiplication and division, ON SIZE ERROR on every operation, and GIVING to preserve operands---is the gold standard for COBOL financial programming.
Summary
COBOL's arithmetic system was designed for one purpose: getting financial calculations exactly right. Its five arithmetic verbs provide both the English-like readability that COBOL is known for and the precision that business demands.
Key principles to remember:
- Use ROUNDED on every calculation that produces a currency amount.
- Use ON SIZE ERROR on every arithmetic operation in production code.
- Use COMP-3 for all financial data fields.
- Use GIVING to preserve operand values.
- Size result fields to accommodate the maximum possible result.
- Check divisors before every division.
- Multiply before dividing to minimize intermediate precision loss.
- Use COMPUTE for complex formulas; use individual verbs for simple operations.
- Use signed fields (PIC S9...) whenever a result might be negative.
- Understand your rounding requirements---banker's rounding (NEAREST-EVEN) is often mandatory for financial applications.
In the next chapter, we will explore conditional logic and program flow control, building on the arithmetic foundation established here to create complete business processing programs.
Code Examples for This Chapter
All code examples are available in the code/ subdirectory:
| File | Description |
|---|---|
example-01-add-subtract.cob |
All forms of ADD and SUBTRACT |
example-02-multiply-divide.cob |
All forms of MULTIPLY and DIVIDE |
example-03-compute.cob |
COMPUTE with expressions and precedence |
example-04-on-size-error.cob |
ON SIZE ERROR, ROUNDED demonstrations |
example-05-financial-calc.cob |
Financial calculations with COMP-3 |
example-06-precision.cob |
Precision, truncation, and USAGE comparison |
exercise-solutions.cob |
Solutions to selected exercises |
case-study-code.cob |
Complete loan payment calculator |