Skip to content
PYD ENTERPRISES

COBOL anti-pattern

COMP-3 packed decimal arithmetic

Fixed-point decimal arithmetic with silent high-order truncation and truncate-or-round rules set per statement, which floating point and careless BigDecimal code do not reproduce.

FAMILY
COBOL
CUTOVER RISK
Critical
Changes runtime behavior in ways a line-by-line reading misses. Resolve before any cutover plan.
MODERN MAPPING
BigDecimal with explicit scale and rounding mode
GENERATED ARTIFACT
openapi.components.yaml

Before and after

legacy source → modern mapping

Legacy: exhibits the pattern

INTACCR.cblCOBOL
1 01 WS-BALANCE PIC S9(9)V99 COMP-3.
2 01 WS-RATE PIC SV9(5) COMP-3.
3 01 WS-DAILY-INT PIC S9(7)V99 COMP-3.
4 01 WS-MONTH-INT PIC S9(7)V99 COMP-3.
5 * 6 + 3 + 5 + 5 BYTES
6 ...
7 COMPUTE WS-DAILY-INT ROUNDED =
8 WS-BALANCE * WS-RATE / 365
9 ADD WS-DAILY-INT TO WS-MONTH-INT
10 COMPUTE WS-FEE = WS-MONTH-INT * 0.015
11 * NO ROUNDED: FRACTION TRUNCATED

Modern: BigDecimal with explicit scale and rounding mode

InterestAccrual.javaJava
1static final MathContext MC = MathContext.DECIMAL128;
2
3BigDecimal dailyInterest(BigDecimal bal, BigDecimal rate) {
4 // COMPUTE ... ROUNDED: half away from zero
5 return bal.multiply(rate, MC)
6 .divide(BigDecimal.valueOf(365), MC)
7 .setScale(2, RoundingMode.HALF_UP);
8}
9
10BigDecimal fee(BigDecimal monthInterest) {
11 // COMPUTE without ROUNDED: truncate
12 return monthInterest
13 .multiply(new BigDecimal("0.015"))
14 .setScale(2, RoundingMode.DOWN);
15}

Why it is dangerous in modernization

COMP-3 stores two decimal digits per byte with a sign nibble, and COBOL arithmetic on it is exact decimal arithmetic with the precision of each receiving field. A COMPUTE without ROUNDED truncates extra fractional digits; with ROUNDED it rounds half away from zero. If the result has more integer digits than the receiving PIC allows and there is no ON SIZE ERROR, the high-order digits are dropped without any error. Intermediate precision depends on the compiler and its ARITH option. Migrations that move these values into double lose cents on ordinary interest and fee calculations, and migrations into BigDecimal often use the wrong rounding mode or skip the truncation to the target scale, so totals drift by a cent on a small fraction of accounts. In regulated calculations, a one-cent drift on a fraction of accounts is still a defect someone has to explain.

How PYD ENTERPRISES detects it

Every numeric field is typed with its digits, scale, sign and usage, and every arithmetic statement is lowered to an expression tree that records the receiving field's precision, the presence of ROUNDED and the presence of ON SIZE ERROR. The engine then computes the maximum magnitude each expression can reach from the declared pictures of its operands and flags statements where the result can exceed the receiver's integer digits with no size error handler. Those are the silent truncation points. Each statement exports with its exact rounding rule, so generated code and generated tests use the same one.

Maps to: BigDecimal with explicit scale and rounding mode

Map each receiving field to a decimal type with a fixed scale: BigDecimal in Java, decimal in C#, a fixed-point decimal library in Go or TypeScript. ROUNDED maps to HALF_UP (round half away from zero), and its absence maps to DOWN (truncate toward zero). Where the legacy code can truncate high-order digits silently, the new code should either reproduce that on purpose or reject the value, and the choice belongs in the reviewed specification.

Generated specification fragment

openapi.components.yamlYAML
1components:
2 schemas:
3 Money9v2:
4 description: PIC S9(9)V99 COMP-3 (6 bytes)
5 type: string
6 pattern: '^-?\d{1,9}\.\d{2}$'
7 x-pyd-scale: 2
8 DailyInterest:
9 description: PIC S9(7)V99 COMP-3 (5 bytes)
10 type: string
11 pattern: '^-?\d{1,7}\.\d{2}$'
12 x-pyd-rounding: HALF_UP
13 x-pyd-source: INTACCR.cbl:7-8

Every generated element links back to the source file and line range shown in the legacy pane. Review and sign off before building against it.

Sizes and the packing rule

A packed field holds one digit per nibble plus a sign nibble, so its length in bytes is (digits + 1) / 2, rounded up. S9(9)V99 has 11 digits: 12 nibbles, 6 bytes. SV9(5) has 5 digits: 6 nibbles, 3 bytes. S9(7)V99 has 9 digits: 10 nibbles, 5 bytes. The sign nibble is C for positive, D for negative and F for unsigned. Even digit counts waste the high-order nibble, which is why experienced mainframe programmers declare odd digit counts.

The implied decimal point (V) is not stored. Scale lives only in the copybook, so a converter that reads the bytes without the picture cannot know whether X'12345C' is 12345, 123.45 or 1.2345.

Why decimal strings in the API contract

JSON numbers are usually parsed into binary floating point by clients, which reintroduces exactly the error the migration is meant to avoid. PYD ENTERPRISES emits money and rate fields as strings with a pattern constrained to the legacy picture, plus an extension recording scale and rounding. Teams that prefer JSON numbers can switch the export, and the generated JSON Schema then carries multipleOf: 0.01 and min and max bounds derived from the picture.

Silent high-order truncation

If WS-MONTH-INT accumulates past 9,999,999.99, the ADD in the example keeps only the low-order seven integer digits, with no error, because there is no ON SIZE ERROR. That may never happen for retail balances and may happen weekly for corporate ones. The engine reports the statement, the maximum reachable magnitude, and the inputs that trigger it, and generates a boundary fixture at the limit and one past it.

Find every COMP-3 packed decimal arithmetic in your codebase.

Run PYD ENTERPRISES in your private VPC and get each occurrence with file, line range, modern mapping and equivalence fixtures, ready for your architects to review.