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 |
|---|---|---|
|
|
All operands before |
|
|
All operands before |
|
|
|
|
|
|
|
|
The value of the expression is stored in every receiver. The |
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-COPIESis 7 + 1 + 10 = 18; withGIVING,WS-COPIESis not a receiver and stays 7. -
SUBTRACT WS-PAID WS-OUT FROM WS-FINE GIVING WS-BALANCEcomputes 12.50 - (10.00 + 1) = 1.50. Everything beforeFROMis 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-VALUEandDIVIDE WS-VALUE BY 4both give a quarter ofWS-VALUE; only the position of the dividend differs. -
DIVIDE 17 BY 7 GIVING WS-QUOTIENT REMAINDER WS-REMAINDERgives 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 |
sign |
2 |
|
exponentiation |
3 |
|
multiplication and division |
4 |
binary |
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-]
Source: Enterprise COBOL 6.5 Language Reference: COMPUTE statement; Programming Guide: Using arithmetic expressions
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 |
|---|---|---|
|
Any discarded fraction rounds to the value of larger magnitude. |
3 and -3 |
|
Nearest value; a tie goes to the larger magnitude. The default for a bare |
3 and -3 |
|
Nearest value; a tie goes to the value whose last digit is even ("banker’s rounding"). |
2 and -2 |
|
Nearest value; a tie goes to the smaller magnitude. |
2 and -2 |
|
No rounding. If the value is not exact the |
undefined |
|
Toward the nearest algebraically larger value. |
3 and -2 |
|
Toward the nearest algebraically smaller value. |
2 and -3 |
|
Discard the fraction; the behaviour without |
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; aNOT ON SIZE ERRORphrase is ignored. If there is no size error theNOT ON SIZE ERRORimperative 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 aPIC 99receiver. -
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 decimalPICTURE, regardless of theTRUNCcompiler option.TRUNCgoverns 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
Source: Enterprise COBOL 6.5 Language Reference: SIZE ERROR phrases; GnuCOBOL Programmer’s Guide: ON SIZE ERROR + NOT ON SIZE ERROR
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 |
|---|---|---|
|
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
Source: Enterprise COBOL 6.5 Programming Guide: Intermediate results and arithmetic precision; Fixed-point data and intermediate results
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
Source: GnuCOBOL 3.2 Manual
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
Source: Enterprise COBOL 6.5 Programming Guide: Fixed-point contrasted with floating-point arithmetic
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
Source: Python documentation: decimal
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-LIMITis one name, and there is no such item. -
WS-SHOWNis numeric-edited, so it cannot be an operand (only aGIVINGorCOMPUTEreceiver may be edited), andWS-NAMEis alphanumeric. De-edit an edited item withFUNCTION NUMVALor keep the number in a numeric item and edit a copy. -
ADD 1 TO 5has a literal as receiver, andMULTIPLY ... BY WS-GROUPhas a group item as receiver. OnlyCORRESPONDINGaccepts 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
ROUNDEDon the statement that produces the final amount, not on intermediate steps, and do multiplications before divisions. -
Put
ON SIZE ERRORon 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-1orCOMP-2. -
Test the dialect: a
cobcdefault build and a-std=ibmbuild can differ in intermediate precision andDISPLAYformat, 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.
References
-
Enterprise COBOL for z/OS 6.5 Language Reference: ADD statement
-
Enterprise COBOL 6.5 Programming Guide: Performing arithmetic
-
Programming Guide: Using COMPUTE and other arithmetic statements
-
Programming Guide: Fixed-point contrasted with floating-point arithmetic
-
Programming Guide: Intermediate results and arithmetic precision