21 min read

Every programming language can add two numbers. What sets COBOL apart is how it adds them---and what happens to the pennies.

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:

  1. Parentheses (innermost first)
  2. ** Exponentiation (highest)
  3. * / Multiplication and Division (left to right)
  4. + - 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:

  1. 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).

  2. Division by zero: Any division where the divisor is zero.

  3. 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

  1. Decimal truncation: When the result has more decimal places than the receiving field. - 1 / 3 = 0.333... stored in PIC V99 becomes 0.33

  2. Integer truncation: When the result has more integer digits than the receiving field (this is a size error). - 999 + 1 = 1000 stored in PIC 999 causes a size error

  3. Intermediate result truncation: When intermediate calculations in a multi-step process lose precision.

Preventing Unwanted Truncation

  1. Use ROUNDED for all currency calculations: cobol MULTIPLY WS-AMOUNT BY WS-RATE GIVING WS-TAX ROUNDED

  2. 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

  3. Use ON SIZE ERROR to detect integer overflow: cobol ADD WS-AMOUNT TO WS-TOTAL ON SIZE ERROR PERFORM 9999-OVERFLOW-HANDLER END-ADD

  4. 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

  1. Exact decimal representation: Unlike binary (COMP) or floating-point (COMP-1/COMP-2), COMP-3 stores decimal values exactly. The value 0.10 is stored as precisely 0.10, not as a binary approximation.

  2. 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.

  3. 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.

  4. 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:

  1. Use ROUNDED on every calculation that produces a currency amount.
  2. Use ON SIZE ERROR on every arithmetic operation in production code.
  3. Use COMP-3 for all financial data fields.
  4. Use GIVING to preserve operand values.
  5. Size result fields to accommodate the maximum possible result.
  6. Check divisors before every division.
  7. Multiply before dividing to minimize intermediate precision loss.
  8. Use COMPUTE for complex formulas; use individual verbs for simple operations.
  9. Use signed fields (PIC S9...) whenever a result might be negative.
  10. 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