Arithmetic and COMPUTE

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against ISO/IEC 1989:2023 (edition 3, January 2023; a 4th edition is upcoming), GnuCOBOL 3.2 and IBM Enterprise COBOL for z/OS 6.5, checked on 2026-10-20. Every program below was compiled with cobc -x -Wall (GnuCOBOL 3.2.0) and run, and compiled without warnings; the deliberately wrong ones show the real diagnostics. Where the output depends on the -std= dialect it says so. Statements that exist only in Enterprise COBOL, such as the ARITH compiler option, are labelled "z/OS; not run here". The examples use the Circulation library scenario of this section: book copies and prices, member fines from the MEMBMAST file with the copybook MEMBREC, and the nightly fine assessment.

Business programs add, multiply and round money, and the whole point of COBOL’s number model is that those operations are exact in decimal. The data is stored as decimal digits (see USAGE and Numeric Representation), a quantity such as 24.90 is held as digits with an implied decimal point (see The PICTURE Clause), and every arithmetic statement is defined in terms of those decimal digits. This page covers the statements, the phrases they share, how big the temporary results are, and where fixed-point decimal arithmetic stops and floating-point arithmetic starts.

The arithmetic statements

COBOL has five arithmetic statements. COMPUTE evaluates a general arithmetic expression; the other four each perform a single operation and are written in a form close to English. All five take the same optional phrases: ROUNDED after a receiving item, and ON SIZE ERROR / NOT ON SIZE ERROR at the end, closed by the matching scope terminator (END-ADD, END-SUBTRACT, END-MULTIPLY, END-DIVIDE, END-COMPUTE).

Statement Forms Result

ADD

ADD a b TO c d; ADD a b TO c GIVING d e; ADD CORRESPONDING g1 TO g2

All operands before TO are summed, and the sum is added to each receiver (format 1). With GIVING the sum of all the operands, including the one after TO, is stored in each GIVING receiver, which takes no part in the sum.

SUBTRACT

SUBTRACT a b FROM c d; SUBTRACT a b FROM c GIVING d; SUBTRACT CORRESPONDING g1 FROM g2

All operands before FROM are summed, and the sum is subtracted from each receiver, or from the single minuend with GIVING.

MULTIPLY

MULTIPLY a BY b c; MULTIPLY a BY b GIVING c d

a is multiplied by each receiver in turn (format 1) and the product replaces it, or the product of a and b is stored in each GIVING receiver.

DIVIDE

DIVIDE a INTO b c; DIVIDE a INTO b GIVING c; DIVIDE a BY b GIVING c; DIVIDE a INTO b GIVING c REMAINDER d; DIVIDE a BY b GIVING c REMAINDER d

INTO divides a into b (b / a), BY divides a by b (a / b). The five formats are those of the Language Reference.

COMPUTE

COMPUTE r1 r2 ROUNDED = expression

The value of the expression is stored in every receiver. The = can be written EQUAL.

The operands of ADD, SUBTRACT, MULTIPLY and DIVIDE are elementary numeric items or numeric literals. A receiver after GIVING (and the REMAINDER receiver) may also be a numeric-edited item, because it takes no part in the computation. The receiver of COMPUTE may be numeric or numeric-edited. A group item is never a valid operand; it is only accepted by the CORRESPONDING forms. The Enterprise COBOL Language Reference also states that if the operands of a statement share storage (they overlap) the result is unpredictable.

The program below shows each statement once. The groups BRANCH-A and BRANCH-B have the same item names, which is what ADD CORRESPONDING needs: it adds the elementary numeric items that have the same name in the two groups, and ignores the rest (here BRANCH-NAME, which is alphanumeric).

       IDENTIFICATION DIVISION.
       PROGRAM-ID. VERBS.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-COPIES            PIC 9(3)    VALUE 5.
       01  WS-RETURNED          PIC 9(3)    VALUE 2.
       01  WS-OUT               PIC 9(3)    VALUE 1.
       01  WS-PRICE             PIC 9(4)V99 VALUE 24.90.
       01  WS-FINE              PIC S9(5)V99 VALUE 12.50.
       01  WS-PAID              PIC 9(3)V99 VALUE 10.00.
       01  WS-BALANCE           PIC S9(5)V99.
       01  WS-TOTAL-COPIES      PIC 9(4).
       01  WS-VALUE             PIC 9(6)V99.
       01  WS-SHARE             PIC 9(4)V99.
       01  WS-QUOTIENT          PIC 9(3).
       01  WS-REMAINDER         PIC 9(3).
       01  WS-WEEKS             PIC 9(3).
       01  WS-DAYS              PIC 9(3).
       01  BRANCH-A.
           05  NEW-LOANS        PIC 9(4) VALUE 120.
           05  RETURNS          PIC 9(4) VALUE 95.
           05  BRANCH-NAME      PIC X(8) VALUE "NORTH".
       01  BRANCH-B.
           05  NEW-LOANS        PIC 9(4) VALUE 80.
           05  RETURNS          PIC 9(4) VALUE 60.
           05  BRANCH-NAME      PIC X(8) VALUE "SOUTH".
       PROCEDURE DIVISION.
       MAIN-PARA.
           ADD WS-RETURNED TO WS-COPIES
           DISPLAY "ADD ... TO          " WS-COPIES
           ADD WS-COPIES WS-OUT 10 GIVING WS-TOTAL-COPIES
           DISPLAY "ADD ... GIVING      " WS-TOTAL-COPIES
           SUBTRACT WS-OUT FROM WS-COPIES
           DISPLAY "SUBTRACT ... FROM   " WS-COPIES
           SUBTRACT WS-PAID WS-OUT FROM WS-FINE GIVING WS-BALANCE
           DISPLAY "SUBTRACT ... GIVING " WS-BALANCE
           MULTIPLY 3 BY WS-COPIES
           DISPLAY "MULTIPLY ... BY     " WS-COPIES
           MULTIPLY WS-PRICE BY WS-COPIES GIVING WS-VALUE
           DISPLAY "MULTIPLY ... GIVING " WS-VALUE
           DIVIDE 4 INTO WS-VALUE GIVING WS-SHARE
           DISPLAY "DIVIDE INTO GIVING  " WS-SHARE
           DIVIDE WS-VALUE BY 4 GIVING WS-SHARE
           DISPLAY "DIVIDE BY GIVING    " WS-SHARE
           DIVIDE 17 BY 7 GIVING WS-QUOTIENT REMAINDER WS-REMAINDER
           DISPLAY "DIVIDE REMAINDER    " WS-QUOTIENT " " WS-REMAINDER
           DIVIDE 23 BY 7 GIVING WS-WEEKS REMAINDER WS-DAYS
           DISPLAY "23 days             " WS-WEEKS " weeks " WS-DAYS
               " days"
           ADD CORRESPONDING BRANCH-B TO BRANCH-A
           DISPLAY "ADD CORR            " NEW-LOANS OF BRANCH-A " "
               RETURNS OF BRANCH-A " " BRANCH-NAME OF BRANCH-A
           STOP RUN.

Compile and run (cobc -x -Wall verbs.cob, no warnings):

ADD ... TO          007
ADD ... GIVING      0018
SUBTRACT ... FROM   006
SUBTRACT ... GIVING +00001.50
MULTIPLY ... BY     018
MULTIPLY ... GIVING 000448.20
DIVIDE INTO GIVING  0112.05
DIVIDE BY GIVING    0112.05
DIVIDE REMAINDER    002 003
23 days             003 weeks 002 days
ADD CORR            0200 0155 NORTH

Points worth noting in that run:

  • ADD WS-COPIES WS-OUT 10 GIVING WS-TOTAL-COPIES is 7 + 1 + 10 = 18; with GIVING, WS-COPIES is not a receiver and stays 7.

  • SUBTRACT WS-PAID WS-OUT FROM WS-FINE GIVING WS-BALANCE computes 12.50 - (10.00 + 1) = 1.50. Everything before FROM is added first, which is not the same as subtracting the operands one after the other from the minuend, although the result is the same here.

  • DIVIDE 4 INTO WS-VALUE and DIVIDE WS-VALUE BY 4 both give a quarter of WS-VALUE; only the position of the dividend differs.

  • DIVIDE 17 BY 7 GIVING WS-QUOTIENT REMAINDER WS-REMAINDER gives 2 and 3. The quotient receiver is an integer, and the remainder is the dividend minus quotient times divisor. In formats 4 and 5 the Language Reference says that the quotient used to calculate the remainder is kept in an intermediate field and is truncated rather than rounded.

  • The remainder phrase cannot be used with floating-point items in Enterprise COBOL. If the quotient overflows, neither the quotient nor the remainder is changed.

The Enterprise COBOL ADD and SUBTRACT formats can also name floating-point items and literals. ADD CORRESPONDING and SUBTRACT CORRESPONDING do not apply to a pair of items when either one has a REDEFINES or OCCURS clause or is an index, a pointer or FILLER. If one of the individual additions raises a size error, the ON SIZE ERROR imperative statement runs only after all the other additions have been done.

When one statement is the same as another, prefer the shorter one: ADD 1 TO COUNT-X reads better than COMPUTE COUNT-X = COUNT-X + 1, and the Programming Guide recommends COMPUTE for anything that combines several operations, because one COMPUTE can replace a series of individual statements and can be more efficient.

COMPUTE and arithmetic expressions

An arithmetic expression is built from numeric literals, numeric data items and numeric intrinsic function references joined by the operators in the table, with parentheses to group. The operators are evaluated in this order, and operators at the same level go from left to right:

Order Operator Meaning

1

unary + and -

sign

2

**

exponentiation

3

* and /

multiplication and division

4

binary + and -

addition and subtraction

Two consequences surprise programmers coming from other languages. A unary minus binds tighter than **, so - A ** 2 is (- A) ** 2, which is positive. And ** is evaluated left to right, so B ** C ** 2 is (B ** C) ** 2, not B ** (C ** 2). Operators must be surrounded by spaces: a hyphen inside a name is part of the name (WS-PRICE-WS-LIMIT is one word, not a subtraction), as the diagnostics section below shows. Write blanks around every +, -, *, / and **.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. COMPUTE1.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-A                 PIC S9(3)V99 VALUE 10.
       01  WS-B                 PIC S9(3)V99 VALUE 4.
       01  WS-C                 PIC S9(3)V99 VALUE 2.
       01  WS-D                 PIC S9(3)V99 VALUE 3.
       01  WS-R1                PIC S9(5)V99.
       01  WS-R2                PIC S9(5)V99.
       01  WS-P1                PIC S9(5).
       01  WS-P2                PIC S9(5).
       01  WS-P3                PIC S9(5)V9.
       01  WS-EDITED            PIC ZZ,ZZ9.99-.
       PROCEDURE DIVISION.
       MAIN-PARA.
           COMPUTE WS-R1 = WS-A + WS-B * WS-C
           COMPUTE WS-R2 = (WS-A + WS-B) * WS-C
           DISPLAY "A + B * C   = " WS-R1
           DISPLAY "(A + B) * C = " WS-R2
           COMPUTE WS-R1 = WS-A - WS-B - WS-C
           DISPLAY "A - B - C   = " WS-R1
           COMPUTE WS-R1 = WS-A / WS-B / WS-C
           DISPLAY "A / B / C   = " WS-R1
           COMPUTE WS-R1 = - WS-A ** 2
           DISPLAY "- A ** 2    = " WS-R1
           COMPUTE WS-R1 = (- WS-A) ** 2
           DISPLAY "(- A) ** 2  = " WS-R1
           COMPUTE WS-R1 = WS-B ** WS-C ** 2
           DISPLAY "B ** C ** 2 = " WS-R1
           COMPUTE WS-P1 WS-P2 ROUNDED WS-P3 = WS-A / WS-B
           DISPLAY "A / B into 3 receivers = " WS-P1 " " WS-P2 " "
               WS-P3
           COMPUTE WS-R1 = WS-A + FUNCTION ABS (WS-C - WS-A)
           DISPLAY "A + ABS (C - A) = " WS-R1
           COMPUTE WS-EDITED = WS-C - WS-A * WS-D
           DISPLAY "edited receiver  [" WS-EDITED "]"
           STOP RUN.
A + B * C   = +00018.00
(A + B) * C = +00028.00
A - B - C   = +00004.00
A / B / C   = +00001.25
- A ** 2    = +00100.00
(- A) ** 2  = +00100.00
B ** C ** 2 = +00256.00
A / B into 3 receivers = +00002 +00003 +00002.5
A + ABS (C - A) = +00018.00
edited receiver  [    28.00-]

The COMPUTE WS-P1 WS-P2 ROUNDED WS-P3 = WS-A / WS-B line shows that ROUNDED belongs to one receiver: 10 / 4 = 2.5 is truncated to 2 in WS-P1, rounded to 3 in WS-P2 and kept exactly in WS-P3. The edited receiver WS-EDITED gets the value with the editing applied, which is how a report line is usually produced (see The PICTURE Clause). Numeric intrinsic functions such as FUNCTION ABS can appear in an expression; they are the subject of Intrinsic Functions.

With cobc -std=ibm, every multi-operand statement in this program produces the warning precision of result may change with arithmetic-osvs [-Warithmetic-osvs]. That is the arithmetic-osvs dialect switch, which GnuCOBOL turns on for IBM compatibility and which changes how intermediate results are sized (see the intermediate results section). Use -Wno-arithmetic-osvs to silence it once you know what it means.

Exponentiation and its size errors

Exponentiation deserves its own rules. A fixed-point base raised to an integer power is computed by repeated multiplication, a negative exponent divides one by the result, and an exponent with decimal places switches the whole evaluation to floating point. Enterprise COBOL truncates a fixed-point exponent of more than nine significant digits to nine. Three combinations are defined as size errors: zero raised to the power zero, zero raised to a negative power, and a negative number raised to a fractional power. Enterprise COBOL’s reaction depends on the SIZE ERROR phrase:

Expression With ON SIZE ERROR Without it

zero raised to zero

the imperative statement runs

the result is 1 and a message is issued

zero raised to a negative number

the imperative statement runs

the program is terminated abnormally

negative number raised to a fractional power

the imperative statement runs

the absolute value of the base is used and a message is issued

GnuCOBOL 3.2.0 follows this for two of the three. With variables (a literal expression such as 0 ** 0 is folded by the compiler, and the ON SIZE ERROR phrase was not executed in a test with literals), zero to the power zero and a negative base to a fractional power both took the ON SIZE ERROR path, but zero to a negative power did not:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. EXPTEST.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-ZERO              PIC 9       VALUE 0.
       01  WS-EXP               PIC S9      VALUE -1.
       01  WS-NEG               PIC S9      VALUE -4.
       01  WS-HALF              PIC 9V9     VALUE 0.5.
       01  WS-R                 PIC S9(5)V99 VALUE 777.
       PROCEDURE DIVISION.
       MAIN-PARA.
           COMPUTE WS-R = WS-ZERO ** WS-ZERO
               ON SIZE ERROR DISPLAY "0**0 size error"
               NOT ON SIZE ERROR DISPLAY "0**0 ok " WS-R
           END-COMPUTE
           COMPUTE WS-R = WS-ZERO ** WS-EXP
               ON SIZE ERROR DISPLAY "0**-1 size error " WS-R
               NOT ON SIZE ERROR DISPLAY "0**-1 ok " WS-R
           END-COMPUTE
           COMPUTE WS-R = WS-NEG ** WS-HALF
               ON SIZE ERROR DISPLAY "(-4)**0.5 size error " WS-R
               NOT ON SIZE ERROR DISPLAY "(-4)**0.5 ok " WS-R
           END-COMPUTE
           COMPUTE WS-R = 2 ** 10
           DISPLAY "2**10 " WS-R
           COMPUTE WS-R = 2 ** -2
           DISPLAY "2**-2 " WS-R
           COMPUTE WS-R = 9 ** 0.5
           DISPLAY "9**0.5 " WS-R
           STOP RUN.
0**0 size error
0**-1 ok +00000.00
(-4)**0.5 size error +00000.00
2**10 +01024.00
2**-2 +00000.25
9**0.5 +00003.00

The values on the last three lines are the ones to rely on: 2 ** 10 is 1024, 2 ** -2 is 0.25, and 9 ** 0.5 is the square root 3. If portable behaviour for the three special cases matters, test the operands before the COMPUTE.

ROUNDED and ROUNDED MODE

After the result of an operation has been aligned on the decimal point of the receiver, any extra low-order fraction digits are discarded. That is truncation, and it is the default. With ROUNDED the receiver’s last digit is increased by one when the first discarded digit is five or more (in magnitude). That is round half away from zero, the rule most accountants expect: 2.5 becomes 3 and -2.5 becomes -3.

Rounding happens before the size check, and Enterprise COBOL may carry one more integer and decimal place in the intermediate result when ROUNDED is written, so that the carry is not lost. ROUNDED has no effect on a floating-point result, which is always rounded to the nearest representable value, and in Enterprise COBOL it is not supported in ARITH(EXTEND) mode for a receiver with 31 digits to the right of the decimal point (PIC V31 or PIC P(30)9).

COBOL 2014 generalised the phrase to ROUNDED MODE IS mode, with eight modes. The GnuCOBOL manual defines the absence of ROUNDED as TRUNCATION and a bare ROUNDED as NEAREST-AWAY-FROM-ZERO. GnuCOBOL 3.2 implements the modes, Enterprise COBOL 6.5 does not: its Language Reference documents only the bare ROUNDED phrase with the half-away-from-zero rule.

Mode Rule 2.5 and -2.5 to an integer

AWAY-FROM-ZERO

Any discarded fraction rounds to the value of larger magnitude.

3 and -3

NEAREST-AWAY-FROM-ZERO

Nearest value; a tie goes to the larger magnitude. The default for a bare ROUNDED.

3 and -3

NEAREST-EVEN

Nearest value; a tie goes to the value whose last digit is even ("banker’s rounding").

2 and -2

NEAREST-TOWARD-ZERO

Nearest value; a tie goes to the smaller magnitude.

2 and -2

PROHIBITED

No rounding. If the value is not exact the EC-SIZE-TRUNCATION condition is set and the result is undefined.

undefined

TOWARD-GREATER

Toward the nearest algebraically larger value.

3 and -2

TOWARD-LESSER

Toward the nearest algebraically smaller value.

2 and -3

TRUNCATION

Discard the fraction; the behaviour without ROUNDED.

2 and -2

The program rounds seven values to an integer in each mode. The columns are, in order: the source, the statement without ROUNDED, and then AWAY-FROM-ZERO, a bare ROUNDED (NEAREST-AWAY-FROM-ZERO), NEAREST-EVEN, NEAREST-TOWARD-ZERO, TOWARD-GREATER, TOWARD-LESSER and TRUNCATION.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. ROUNDED.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-SRC               PIC S9V999.
       01  WS-R-PLAIN           PIC S9.
       01  WS-R-AWAY            PIC S9.
       01  WS-R-NAFZ            PIC S9.
       01  WS-R-EVEN            PIC S9.
       01  WS-R-NTZ             PIC S9.
       01  WS-R-GREATER         PIC S9.
       01  WS-R-LESSER          PIC S9.
       01  WS-R-TRUNC           PIC S9.
       01  WS-SHOW              PIC -9.999.
       01  WS-I                 PIC 9.
       01  WS-VALUES.
           05  FILLER           PIC S9V999 VALUE +3.510.
           05  FILLER           PIC S9V999 VALUE +2.500.
           05  FILLER           PIC S9V999 VALUE +3.500.
           05  FILLER           PIC S9V999 VALUE +2.499.
           05  FILLER           PIC S9V999 VALUE -2.500.
           05  FILLER           PIC S9V999 VALUE -3.500.
           05  FILLER           PIC S9V999 VALUE -3.510.
       01  WS-TABLE REDEFINES WS-VALUES.
           05  WS-ENTRY         PIC S9V999 OCCURS 7 TIMES.
       PROCEDURE DIVISION.
       MAIN-PARA.
           DISPLAY "source   plain  away   n-away n-even n-zero"
               " greater lesser trunc"
           PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 7
               MOVE WS-ENTRY (WS-I) TO WS-SRC
               COMPUTE WS-R-PLAIN = WS-SRC
               COMPUTE WS-R-AWAY ROUNDED MODE IS AWAY-FROM-ZERO
                   = WS-SRC
               COMPUTE WS-R-NAFZ ROUNDED = WS-SRC
               COMPUTE WS-R-EVEN ROUNDED MODE IS NEAREST-EVEN
                   = WS-SRC
               COMPUTE WS-R-NTZ ROUNDED MODE IS NEAREST-TOWARD-ZERO
                   = WS-SRC
               COMPUTE WS-R-GREATER ROUNDED MODE IS TOWARD-GREATER
                   = WS-SRC
               COMPUTE WS-R-LESSER ROUNDED MODE IS TOWARD-LESSER
                   = WS-SRC
               COMPUTE WS-R-TRUNC ROUNDED MODE IS TRUNCATION
                   = WS-SRC
               MOVE WS-SRC TO WS-SHOW
               DISPLAY WS-SHOW "   " WS-R-PLAIN "     " WS-R-AWAY
                   "      " WS-R-NAFZ "      " WS-R-EVEN "      "
                   WS-R-NTZ "      " WS-R-GREATER "      " WS-R-LESSER
                   "      " WS-R-TRUNC
           END-PERFORM
           STOP RUN.
source   plain  away   n-away n-even n-zero greater lesser trunc
 3.510   +3     +4      +4      +4      +4      +4      +3      +3
 2.500   +2     +3      +3      +2      +2      +3      +2      +2
 3.500   +3     +4      +4      +4      +3      +4      +3      +3
 2.499   +2     +3      +2      +2      +2      +3      +2      +2
-2.500   -2     -3      -3      -2      -2      -2      -3      -2
-3.500   -3     -4      -4      -4      -3      -3      -4      -3
-3.510   -3     -4      -4      -4      -4      -3      -4      -3

Rows 2 and 3 are the ties, 2.500 and 3.500; reading across them shows the difference between the nearest modes. NEAREST-EVEN sends both to the even integer (2 and 4), which is why it is preferred where thousands of roundings are summed: the errors cancel instead of drifting upwards. PROHIBITED is not in the run because its result is undefined.

The default mode for a whole program can be changed in the OPTIONS paragraph of the IDENTIFICATION DIVISION, which GnuCOBOL 3.2 supports with a DEFAULT ROUNDED MODE IS clause. It then applies to every ROUNDED phrase without a MODE:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. OPTS.
       OPTIONS.
           DEFAULT ROUNDED MODE IS NEAREST-EVEN.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-A                 PIC S9V9 VALUE +2.5.
       01  WS-B                 PIC S9.
       01  WS-C                 PIC S9.
       PROCEDURE DIVISION.
       MAIN-PARA.
           COMPUTE WS-B ROUNDED = WS-A
           COMPUTE WS-C = WS-A
           DISPLAY "ROUNDED default mode: " WS-B
           DISPLAY "no ROUNDED:           " WS-C
           STOP RUN.
ROUNDED default mode: +2
no ROUNDED:           +2

The bare ROUNDED gave 2 because the default mode is now NEAREST-EVEN; the statement without ROUNDED truncates to 2 as well. Two cautions from the same test. The manual’s syntax diagram for ROUNDED MODE spells the modes NEAREST-TOWARD-ZERO and TOWARD-GREATER, but its OPTIONS diagram spells them NEAREST-TOWARDS-ZERO and TOWARDS-GREATER; GnuCOBOL 3.2.0 accepted the first spelling and rejected the second in the OPTIONS paragraph. And the mode names are a dialect matter: the same rounded.cob compiles and gives the same numbers with -std=ibm, but -std=ibm-strict rejects ROUNDED MODE with syntax error, unexpected Identifier, which is the honest answer for Enterprise COBOL 6.5.

If you need a mode other than half-away-from-zero on z/OS, say banker’s rounding, there are two ways. The first is to add ROUNDED to a COMPUTE that carries enough decimals and test the dropped digits yourself. The second is the intrinsic function route: the FUNCTION INTEGER-PART, FUNCTION INTEGER (greatest integer not greater than its argument) and FUNCTION REM functions give the pieces of a hand-written rounding rule (see Intrinsic Functions). Which is easier depends on the case; what you should not do is round by moving the value to a shorter field, since MOVE truncates.

ON SIZE ERROR and NOT ON SIZE ERROR

A size error happens when the absolute value of the result, after decimal point alignment, does not fit in the receiver, and when a division by zero is attempted. Exponentiation adds the three cases listed above. The phrase applies to the final result only, never to an intermediate result.

What happens depends on whether the phrase is written:

  • Without ON SIZE ERROR, the result is truncated on the left by the usual rules for storing into a smaller field. The arithmetic statement does not complain. The Language Reference only says that "truncation rules apply" for this case.

  • With ON SIZE ERROR, the receiver that overflowed is left unchanged, the imperative statement runs, and control passes to the end of the arithmetic statement; a NOT ON SIZE ERROR phrase is ignored. If there is no size error the NOT ON SIZE ERROR imperative statement runs instead.

  • With several receivers, only the receivers that overflow are left alone; the others are stored, and the phrase runs once after all of them have been processed.

  • With ROUNDED, rounding happens first, so a result such as 99.6 that rounds up to 100 is a size error for a PIC 99 receiver.

  • For DIVIDE ... REMAINDER, a size error in the quotient leaves both the quotient and the remainder unchanged, and a size error in the remainder leaves the remainder unchanged; you must test which happened.

  • For a binary receiver (BINARY, COMP, COMP-4, COMP-5), the largest value the item can hold is the one implied by the decimal PICTURE, regardless of the TRUNC compiler option. TRUNC governs only what happens when no size-error phrase is present; see USAGE and Numeric Representation.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. SIZEERR.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-COPIES            PIC 9(3)    VALUE 995.
       01  WS-SMALL             PIC 99.
       01  WS-PAIR-A            PIC 99      VALUE 90.
       01  WS-PAIR-B            PIC 999     VALUE 90.
       01  WS-ZERO              PIC 9       VALUE 0.
       01  WS-QUOTIENT          PIC 9(3)    VALUE 777.
       01  WS-REMAINDER         PIC 9(3)    VALUE 888.
       PROCEDURE DIVISION.
       MAIN-PARA.
      *    No ON SIZE ERROR: the high-order digits are lost
           ADD 10 TO WS-COPIES
           DISPLAY "1 no phrase, 995 + 10 in PIC 9(3):   " WS-COPIES
      *    ON SIZE ERROR: the receiving field is left alone
           MOVE 995 TO WS-COPIES
           ADD 10 TO WS-COPIES
               ON SIZE ERROR
                   DISPLAY "2 size error, field unchanged:       "
                       WS-COPIES
               NOT ON SIZE ERROR
                   DISPLAY "2 ok"
           END-ADD
      *    ROUNDED happens before the size check
           COMPUTE WS-SMALL ROUNDED = 99.6
               ON SIZE ERROR
                   DISPLAY "3 99.6 rounded to 100 does not fit PIC 99"
           END-COMPUTE
      *    A size error on one receiver leaves only that receiver alone
           ADD 15 TO WS-PAIR-A WS-PAIR-B
               ON SIZE ERROR
                   DISPLAY "4 PAIR-A " WS-PAIR-A " PAIR-B " WS-PAIR-B
           END-ADD
      *    Division by zero is a size error
           DIVIDE 10 BY WS-ZERO GIVING WS-QUOTIENT
               REMAINDER WS-REMAINDER
               ON SIZE ERROR
                   DISPLAY "5 divide by zero, quotient " WS-QUOTIENT
                       " remainder " WS-REMAINDER
           END-DIVIDE
           STOP RUN.
1 no phrase, 995 + 10 in PIC 9(3):   005
2 size error, field unchanged:       995
3 99.6 rounded to 100 does not fit PIC 99
4 PAIR-A 90 PAIR-B 105
5 divide by zero, quotient 777 remainder 888

Line 1 is the dangerous one: 995 + 10 = 1005 does not fit in three digits, nothing is reported, and the stored value is 005. Line 4 shows that the two receivers of one ADD are treated separately in GnuCOBOL: WS-PAIR-A (PIC 99, 90 + 15 = 105) is left at 90 while WS-PAIR-B (PIC 999) becomes 105. Line 5 is the division by zero: neither the quotient (777) nor the remainder (888) is touched.

On z/OS, ON SIZE ERROR for fixed-point overflow and decimal overflow works only when the Language Environment run-time option TRAP(ON) is in effect. Floating-point exponent overflow does not cause a size error: it abends, and the way to intercept it is a user-written condition handler. The Programming Guide’s example handles a division by zero in a paragraph with ON SIZE ERROR, displays the operands and stops the run.

What about a division by zero with no ON SIZE ERROR phrase at all? The GnuCOBOL manual says the run time prints an error and aborts the program. GnuCOBOL 3.2.0 did not do that in a test with a DIVIDE of 120 by a zero field: the program carried on, the receiver kept its old value and the exit status was 0, with or without -debug:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. DIVZERO.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-TOTAL             PIC 9(5)V99 VALUE 120.
       01  WS-COUNT             PIC 9(3)    VALUE 0.
       01  WS-AVERAGE           PIC 9(5)V99 VALUE 7.
       PROCEDURE DIVISION.
       MAIN-PARA.
           DIVIDE WS-TOTAL BY WS-COUNT GIVING WS-AVERAGE
           DISPLAY "average " WS-AVERAGE
           STOP RUN.
average 00007.00

The lesson is the same on every platform: do not rely on a missing phrase to stop the program. Either test the divisor, or write ON SIZE ERROR and decide there what the program does.

Intermediate results and precision

A COMPUTE with several operators is evaluated as a series of two-operand steps, in precedence order, each storing its result in a compiler-generated temporary field called an intermediate result. For COMPUTE Y = A + B * C - D / E + F ** G Enterprise COBOL exponentiates F by G, multiplies B by C, divides D by E, then adds, subtracts and adds in turn. Intermediate results also arise in ADD or SUBTRACT with several operands, in a statement with GIVING and several receivers, in arithmetic inside a condition, and when ROUNDED is written.

How many digits a temporary field has is the compiler’s decision, and it is the source of most unexpected results. In Enterprise COBOL, for fixed-point data, the Programming Guide gives the theoretically possible size of each operation:

Operation Integer places Decimal places

+ or -

larger of the operands' integer places, plus 1

larger of the operands' decimal places

*

i1 + i2

d1 + d2

/

i2 + d1

larger of (d2 - d1) and dmax

dmax is the largest number of decimal places among the receivers and the operands (excluding divisors and exponents). The compiler never carries more than 30 digits in ARITH(COMPAT), the default, or 31 in ARITH(EXTEND). When a result needs more, the compiler keeps the decimal places down to dmax and lets the integer part use the remaining digits, and if even that is too much the extra digits are truncated and a warning is issued (at run time, a message and the program goes on). The lesson is to declare operands and receivers with enough decimal places for the accuracy you want. The classic trap is division followed by multiplication.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. INTERM.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-A                 PIC 9(3) VALUE 10.
       01  WS-B                 PIC 9(3) VALUE 3.
       01  WS-C                 PIC 9(3) VALUE 3.
       01  WS-R1                PIC 9(3)V99.
       01  WS-R2                PIC 9(3)V99.
       01  WS-R3                PIC 9(3)V99.
       01  WS-R4                PIC 9(3).
       PROCEDURE DIVISION.
       MAIN-PARA.
           COMPUTE WS-R1 = WS-A / WS-B * WS-C
           COMPUTE WS-R2 = WS-A * WS-C / WS-B
           COMPUTE WS-R3 = (WS-A / WS-B) * WS-C
           COMPUTE WS-R4 ROUNDED = WS-A / WS-B * WS-C
           DISPLAY "A / B * C   = " WS-R1
           DISPLAY "A * C / B   = " WS-R2
           DISPLAY "(A / B) * C = " WS-R3
           DISPLAY "rounded     = " WS-R4
           STOP RUN.
A / B * C   = 009.99
A * C / B   = 010.00
(A / B) * C = 009.99
rounded     = 010

A / B * C divides first and stores 3.33 (two decimals, the receiver’s), so the product is 9.99, not 10.00. Doing the multiplication first, A * C / B, or changing the order of the operands, gives exactly 10.00. The parenthesised form is the same as the first because the divide is already first. With a plain ROUNDED and an integer receiver, the default GnuCOBOL arithmetic rounds the 9.99 to 10; a compiler that follows the Enterprise COBOL rules to the letter carries no decimals at all when every operand and the receiver are integers, so the division gives 3 and the product 9. That is what -std=ibm (which enables the arithmetic-osvs rules) printed for the same program:

A / B * C   = 00999
A * C / B   = 01000
(A / B) * C = 00999
rounded     = 009

Under -std=ibm DISPLAY shows implied-decimal fields without the decimal point, so 00999 is 9.99 and 01000 is 10.00, while 009 is the integer 9. The compile also emitted four -Warithmetic-osvs warnings, one per COMPUTE. So the same source gave 10 in the default dialect and 9 in the IBM-like dialect: for a program that must match the mainframe to the penny, compile the test build with -std=ibm, write the multiplication before the division, and give the receiver the decimals you need.

Two other rules from the Appendix are worth knowing. If a binary operation needs an intermediate result of more than 18 digits, the compiler converts the operands to internal decimal, performs the operation, and converts a binary result back. And "the composite of operands" of an ADD or SUBTRACT (all the operands aligned on the decimal point and superimposed) must not exceed 30 digits in ARITH(COMPAT) or 31 in ARITH(EXTEND); MULTIPLY and DIVIDE apply the limit to the receivers, and COMPUTE has no such limit. For example, ADD A B TO C with A as 9(7)V9(5), B as 9(11)V99 and C as 9(12)V9(3) has a composite of 9(12)V9(5), 17 digits.

ARITH(COMPAT) and ARITH(EXTEND)

ARITH is an Enterprise COBOL compiler option (z/OS; not run here). With the default ARITH(COMPAT) the maximum size of a PICTURE for packed-decimal, external-decimal and numeric-edited items is 18 digits, fixed-point literals can have 18 digits, NUMVAL and its relatives take 18-digit arguments, the maximum argument of FACTORIAL is 28, and fixed-point intermediate results use at most 30 digits. With ARITH(EXTEND) all of those limits go up to 31 (29 for FACTORIAL), the intermediate results use up to 31 digits, and floating-point intrinsic functions return extended-precision (128-bit) results, and expressions with floating-point operands are evaluated in extended-precision floating point. ARITH(EXTEND) costs run time and is chosen per program, either on the CBL or PROCESS statement or in the compile procedure:

       CBL ARITH(EXTEND)
       IDENTIFICATION DIVISION.
       PROGRAM-ID. BIGSUM.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-CHAR              PIC X(30)
                                VALUE "123456789012345678901234567890".
       01  WS-NUM               PIC 9(30).
       PROCEDURE DIVISION.
           MOVE WS-CHAR TO WS-NUM
           DISPLAY WS-NUM
           GOBACK.

z/OS; not run here. With ARITH(COMPAT) the PIC 9(30) item is a syntax error, because 30 digits is more than the 18 allowed.

GnuCOBOL has no ARITH option and needs none. Numeric items can have up to 38 digits (binary items up to 18 digits by default), and in these tests the arithmetic behaved as if the equivalent of ARITH(EXTEND) were always on:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. WIDE.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-BASE              PIC 9(2)  VALUE 2.
       01  WS-POWER             PIC 9(3)  VALUE 100.
       01  WS-BIG               PIC 9(31).
       01  WS-MILLS             PIC 9(27)V9(4)
                                VALUE 1234567890123456789.1234.
       01  WS-DOUBLED           PIC 9(27)V9(4).
       01  WS-HUGE              PIC 9(38).
       PROCEDURE DIVISION.
       MAIN-PARA.
           COMPUTE WS-BIG = WS-BASE ** WS-POWER
           DISPLAY "2 ** 100       = " WS-BIG
           COMPUTE WS-DOUBLED = WS-MILLS * 2
           DISPLAY "wide fraction  = " WS-DOUBLED
           MOVE ALL "9" TO WS-HUGE
           ADD 1 TO WS-HUGE
               ON SIZE ERROR DISPLAY "38 digits is the limit"
           END-ADD
           STOP RUN.
2 ** 100       = 1267650600228229401496703205376
wide fraction  = 000000002469135780246913578.2468
38 digits is the limit

2 ** 100 has 31 digits and fits a PIC 9(31). The last two lines show a 31-digit item with four decimals and the first size error of a 38-digit item. The same program also compiles with -std=ibm and -std=ibm-strict, which is a reminder that GnuCOBOL’s range is wider than Enterprise COBOL’s even in the IBM dialects: code that depends on GnuCOBOL accepting a PIC 9(31) will not compile on z/OS without ARITH(EXTEND), and anything over 31 digits will not compile there at all.

Fixed-point decimal versus floating point

Enterprise COBOL decides per operation whether to use fixed-point or floating-point arithmetic. An operation is floating-point when an operand or the result is floating point (a floating-point literal or a COMP-1, COMP-2 or external floating-point item), or when an exponent has decimal places, or when an intrinsic function that returns floating point is involved. Otherwise it is fixed-point: all operands and the result are fixed-point, no exponent has decimal places, and nested expressions and function references are fixed-point as well. Comparisons are decided the same way, over both comparands together. For example COMPUTE X = (N ** .5) + 1 is a floating-point evaluation because of the fractional exponent, while ADD A TO B GIVING C is a fixed-point one. In a floating-point evaluation ROUNDED is ignored.

Binary floating point cannot represent most decimal fractions exactly, which is why money is not kept in COMP-1 or COMP-2. The program adds a dime a thousand times, once into a double-precision floating-point item and once into packed decimal:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. DECFLOAT.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-FLOAT-TOTAL       COMP-2 VALUE 0.
       01  WS-FLOAT-DIME        COMP-2 VALUE 0.1.
       01  WS-FIXED-TOTAL       PIC S9(5)V99 COMP-3 VALUE 0.
       01  WS-FIXED-DIME        PIC S9V99 COMP-3 VALUE 0.10.
       01  WS-I                 PIC 9(4) COMP.
       01  WS-SHOW              PIC -(5)9.9(16).
       PROCEDURE DIVISION.
       MAIN-PARA.
           PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 1000
               ADD WS-FLOAT-DIME TO WS-FLOAT-TOTAL
               ADD WS-FIXED-DIME TO WS-FIXED-TOTAL
           END-PERFORM
           MOVE WS-FLOAT-TOTAL TO WS-SHOW
           DISPLAY "COMP-2 total:  " WS-SHOW
           DISPLAY "COMP-2 raw:    " WS-FLOAT-TOTAL
           DISPLAY "COMP-3 total:  " WS-FIXED-TOTAL
           IF WS-FIXED-TOTAL = 100
               DISPLAY "fixed-point total is exactly 100"
           END-IF
           STOP RUN.
COMP-2 total:      99.9999999999957367
COMP-2 raw:    99.99999999999574
COMP-3 total:  +00100.00
fixed-point total is exactly 100

The floating-point total is 99.99999999999574 instead of 100. (The exact digits depend on the platform: GnuCOBOL does its COMP-2 arithmetic through a decimal intermediate, and on z/OS the same loop would be done with the hardware’s own floating-point format, so the error is of the same kind but not the same digits.) The packed-decimal total is exactly 100.00, and the IF is true. Comparing floating-point values with = is therefore a bug waiting to happen. If you must use floating point, for example for a scientific calculation, compare with a tolerance, and move the final result to a fixed-point field with COMPUTE ... ROUNDED before it is stored or printed. The Programming Guide also suggests floating point to avoid truncation of intermediate results that exceed 30 digits, which is one of the few reasons to prefer it.

Decimal arithmetic in other languages

The same issue exists in every language with binary floating point, and the cure is the same: use a decimal type. Java’s BigDecimal does what COBOL’s PIC 9(n)V99 and ROUNDED do, with the rounding mode and scale explicit in the code; its RoundingMode.HALF_UP corresponds to the bare ROUNDED and HALF_EVEN to NEAREST-EVEN. The page Numbers and Math covers BigDecimal, MathContext and the new BigDecimal(double) trap. Python’s decimal.Decimal is the same idea; the page Numbers covers it. In Python (3.9, run here), the same experiment as above is:

from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP

print(sum([0.1] * 1000))                       # float
print(sum([Decimal("0.10")] * 1000))           # decimal
print(Decimal("2.5").quantize(Decimal("1"), rounding=ROUND_HALF_EVEN),
      Decimal("2.5").quantize(Decimal("1"), rounding=ROUND_HALF_UP))
99.9999999999986
100.00
2 3

The difference is that in COBOL the decimal type is the ordinary numeric data item, the scale is part of its declaration, and all arithmetic on it is decimal by default; the other languages ask the programmer to select the type.

The Circulation fine calculation

The nightly run assesses fines for late returns. The rules of this scenario are: a late return costs 0.35 per day; a fine for one return is capped at 5.00; members who joined before 2021 get a loyalty reduction of 12.5 percent; and the amount is rounded to the cent and added to the member’s fine total in MEMBER-FINES. The member records come from MEMBMAST through the copybook MEMBREC; the days late would normally come from the loan file, and are set in the program here so that the example is self-contained.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. FINES.
       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT MEMBER-FILE ASSIGN TO "MEMBMAST"
               ORGANIZATION IS LINE SEQUENTIAL.
       DATA DIVISION.
       FILE SECTION.
       FD  MEMBER-FILE.
           COPY MEMBREC.
       WORKING-STORAGE SECTION.
       01  WS-EOF               PIC X       VALUE "N".
           88  AT-END                       VALUE "Y".
       01  WS-DAILY-RATE        PIC 9V99    VALUE 0.35.
       01  WS-MAX-FINE          PIC 9(3)V99 VALUE 5.00.
       01  WS-LOYALTY-RATE      PIC V999    VALUE .125.
       01  WS-DAYS-LATE         PIC 9(3).
       01  WS-FINE              PIC 9(3)V99.
       01  WS-SHOW-BEFORE       PIC -(5)9.99.
       01  WS-SHOW-FINE         PIC ZZ9.99.
       01  WS-SHOW-AFTER        PIC -(5)9.99.
       PROCEDURE DIVISION.
       MAIN-PARA.
           OPEN INPUT MEMBER-FILE
           PERFORM UNTIL AT-END
               READ MEMBER-FILE
                   AT END SET AT-END TO TRUE
                   NOT AT END PERFORM ASSESS-FINE
               END-READ
           END-PERFORM
           CLOSE MEMBER-FILE
           STOP RUN.
       ASSESS-FINE.
           EVALUATE MEMBER-ID
               WHEN 100 MOVE 3  TO WS-DAYS-LATE
               WHEN 101 MOVE 20 TO WS-DAYS-LATE
               WHEN OTHER MOVE 7 TO WS-DAYS-LATE
           END-EVALUATE
           MOVE MEMBER-FINES TO WS-SHOW-BEFORE
           IF MEMBER-JOINED < 20210101
               COMPUTE WS-FINE ROUNDED =
                   WS-DAYS-LATE * WS-DAILY-RATE * (1 - WS-LOYALTY-RATE)
                   ON SIZE ERROR MOVE WS-MAX-FINE TO WS-FINE
               END-COMPUTE
           ELSE
               COMPUTE WS-FINE ROUNDED = WS-DAYS-LATE * WS-DAILY-RATE
                   ON SIZE ERROR MOVE WS-MAX-FINE TO WS-FINE
               END-COMPUTE
           END-IF
           IF WS-FINE > WS-MAX-FINE
               MOVE WS-MAX-FINE TO WS-FINE
           END-IF
           ADD WS-FINE TO MEMBER-FINES
               ON SIZE ERROR
                   DISPLAY MEMBER-ID " fine total does not fit"
           END-ADD
           MOVE WS-FINE TO WS-SHOW-FINE
           MOVE MEMBER-FINES TO WS-SHOW-AFTER
           DISPLAY MEMBER-ID " " MEMBER-NAME " days " WS-DAYS-LATE
               " before " WS-SHOW-BEFORE " fine " WS-SHOW-FINE
               " after " WS-SHOW-AFTER.
000100 ADA LOVELACE                   days 003 before      0.00 fine   0.92 after      0.92
000101 GRACE HOPPER                   days 020 before     12.50 fine   5.00 after     17.50
000102 ALAN TURING                    days 007 before     50.00 fine   2.14 after     52.14

Several decisions in the program are worth reading across. The rate, cap and loyalty percentage are items with a VALUE, not literals scattered through the procedure code. The loyalty reduction is a fraction with its own PICTURE (V999), so the expression 1 - WS-LOYALTY-RATE is exact in decimal. The fine is computed in one COMPUTE ROUNDED, so the only rounding is the final one: Ada’s three days at 0.35 with the 12.5 percent reduction is 0.91875, rounded once to 0.92. The cap is an IF after the computation; Grace’s 20 days would be 7.00. The ADD ... ON SIZE ERROR protects the master record, since a member total that would not fit in S9(5)V99 is reported instead of truncated. All the other statements use a PERFORM and EVALUATE that are described in PERFORM and Structured Programming and Conditions, IF and EVALUATE.

Under -std=ibm the program prints the same lines, preceded by the -Warithmetic-osvs warnings.

Pitfalls and real diagnostics

The deliberately wrong program below collects the compile-time errors that new COBOL programmers meet most often:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. BAD.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-PRICE             PIC 9(4)V99 VALUE 24.90.
       01  WS-TAX               PIC 9(4)V99.
       01  WS-SHOWN             PIC ZZZ9.99.
       01  WS-NAME              PIC X(10)   VALUE "ADA".
       01  WS-LIMIT             PIC 9(3)    VALUE 3.
       01  WS-GROUP.
           05  WS-G1            PIC 99 VALUE 1.
           05  WS-G2            PIC 99 VALUE 2.
       PROCEDURE DIVISION.
       MAIN-PARA.
           COMPUTE WS-TAX = WS-PRICE-WS-LIMIT
           ADD WS-SHOWN TO WS-TAX
           COMPUTE WS-TAX = WS-PRICE + WS-NAME
           ADD 1 TO 5
           MULTIPLY WS-PRICE BY WS-GROUP
           STOP RUN.

cobc -x -Wall bad.cob stops with:

bad.cob: in paragraph 'MAIN-PARA':
bad.cob:15: error: 'WS-PRICE-WS-LIMIT' is not defined
   13 |        PROCEDURE DIVISION.
   14 |        MAIN-PARA.
   15 >            COMPUTE WS-TAX = WS-PRICE-WS-LIMIT
   16 |            ADD WS-SHOWN TO WS-TAX
   17 |            COMPUTE WS-TAX = WS-PRICE + WS-NAME
bad.cob:16: error: 'WS-SHOWN' is not numeric
   14 |        MAIN-PARA.
   15 |            COMPUTE WS-TAX = WS-PRICE-WS-LIMIT
   16 >            ADD WS-SHOWN TO WS-TAX
   17 |            COMPUTE WS-TAX = WS-PRICE + WS-NAME
   18 |            ADD 1 TO 5
bad.cob:17: error: 'WS-NAME' is not numeric
   15 |            COMPUTE WS-TAX = WS-PRICE-WS-LIMIT
   16 |            ADD WS-SHOWN TO WS-TAX
   17 >            COMPUTE WS-TAX = WS-PRICE + WS-NAME
   18 |            ADD 1 TO 5
   19 |            MULTIPLY WS-PRICE BY WS-GROUP
bad.cob:18: error: '5' is not a numeric name
   16 |            ADD WS-SHOWN TO WS-TAX
   17 |            COMPUTE WS-TAX = WS-PRICE + WS-NAME
   18 >            ADD 1 TO 5
   19 |            MULTIPLY WS-PRICE BY WS-GROUP
   20 |            STOP RUN.
bad.cob:19: error: 'WS-GROUP' is not a numeric name
   17 |            COMPUTE WS-TAX = WS-PRICE + WS-NAME
   18 |            ADD 1 TO 5
   19 >            MULTIPLY WS-PRICE BY WS-GROUP
   20 |            STOP RUN.
   21 | <EOF>
  • The first error is the missing spaces around a minus sign: WS-PRICE-WS-LIMIT is one name, and there is no such item.

  • WS-SHOWN is numeric-edited, so it cannot be an operand (only a GIVING or COMPUTE receiver may be edited), and WS-NAME is alphanumeric. De-edit an edited item with FUNCTION NUMVAL or keep the number in a numeric item and edit a copy.

  • ADD 1 TO 5 has a literal as receiver, and MULTIPLY ... BY WS-GROUP has a group item as receiver. Only CORRESPONDING accepts groups.

The run-time pitfall is a numeric item that holds something that is not a number. A REDEFINES of a record read from a file over a numeric field, or MOVE SPACES over a group that contains numeric items, leaves bytes in the field that are not digits. On z/OS the arithmetic on such a field is the classic S0C7 data exception abend, and the NUMCHECK compiler option can be used to catch it. GnuCOBOL does not abend by default; it treats the bytes as digits:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. BADDATA.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-RAW               PIC X(5) VALUE "  12 ".
       01  WS-NUM REDEFINES WS-RAW PIC 9(5).
       01  WS-TOTAL             PIC 9(5) VALUE 100.
       PROCEDURE DIVISION.
       MAIN-PARA.
           IF WS-NUM NOT NUMERIC
               DISPLAY "not numeric: " WS-RAW
           END-IF
           ADD WS-NUM TO WS-TOTAL
           DISPLAY "total " WS-TOTAL
           STOP RUN.
not numeric:   12
total 00220

The five bytes " 12 " were read as the digits 0 0 1 2 0, which is 120, and the total became 220. The IF WS-NUM NOT NUMERIC test is the way to catch the data before it is used. Compiled with -debug, GnuCOBOL stops at the ADD:

not numeric:   12
libcob: baddata.cob:13: error: 'WS-NUM' (Type: NUMERIC DISPLAY) not numeric: '  12 '

 Last statement of "BADDATA" was ADD
	MAIN-PARA at baddata.cob:13
	ENTRY BADDATA at baddata.cob:9
 Started by ./baddata.out

The statements that move data into numeric fields, and the S0C7 cases they lead to, are on MOVE and Data Movement; the compiler options that change the behaviour (TRUNC, NUMPROC, NUMCHECK, ARITH) are on Performance and Compiler Options. A final list of habits that avoid arithmetic bugs:

  • Give every receiver enough integer digits for the largest value it can see and enough decimals for the accuracy needed.

  • Write ROUNDED on the statement that produces the final amount, not on intermediate steps, and do multiplications before divisions.

  • Put ON SIZE ERROR on every statement whose divisor or result size is not guaranteed, and have the imperative statement do something deliberate.

  • Keep money in fixed-point decimal (zoned or packed), not in COMP-1 or COMP-2.

  • Test the dialect: a cobc default build and a -std=ibm build can differ in intermediate precision and DISPLAY format, and neither is the mainframe.

Where the book is outdated

Tom Taulli’s Modern Mainframe Development introduces ADD, SUBTRACT, MULTIPLY, DIVIDE and COMPUTE at tutorial level, and that part is still accurate: the statements and their basic forms have not changed. What it cannot show, because it was written against Enterprise COBOL V6.3 as the latest release, is the current picture: 6.5 as the compiler, the COBOL 2014 ROUNDED MODE values (which Enterprise COBOL 6.5 still does not implement, but GnuCOBOL 3.2 does), the full rules for intermediate results and ARITH(EXTEND), the TRAP(ON) requirement for ON SIZE ERROR on z/OS, and the dialect differences between GnuCOBOL and the mainframe compiler that matter when a program is developed on one and run on the other.