COBOL Textbook

COBOL input validation: class tests, INSPECT and TEST-NUMVAL

A GnuCOBOL edit program that checks every field of a fixed-width feed with CLASS, INSPECT, TEST-NUMVAL and TEST-DATE-YYYYMMDD, and logs every error with a code.

Published
Updated
Dialect
GnuCOBOL 3.x, free format
Status
Compiled and run with GnuCOBOL 3.2
Length
3,747 words, 17 min

An edit program stands between data from another system and everything downstream of it. It reads what was sent, decides record by record what may go on to the next step, and writes a list a person can fix the rest from. The bank-statement job earlier in this part did a small version of that: it stopped at the first fault in each record, and its own limitations section admits that it accepts 20261399 as a date. This lesson builds the full version for an 80-byte payment feed. The program checks every field of every record that reads cleanly, writes every error it finds with a numbered code, passes the clean records on in a layout the next program can do arithmetic on, and ends with totals that account for every record read.

What was run and what was not. cobc was not installed on the machine this lesson was first prepared on, so we desk-checked the listing against the GnuCOBOL Programmer's Guide, which documents the July 2020 build of GnuCOBOL 3.1 RC-1, and against the source code, test suite and NEWS file of the GnuCOBOL 3.2 release, downloaded from the GNU project's server on October 8, 2026, where 3.2 is still the newest release. The expected output below comes from the Python companion, /code/cobol-input-validation-class-inspect-test-numval.py. It copies each runtime rule the program depends on from the 3.2 source, checks the copies against GnuCOBOL's own tests and IBM's documented examples wherever those exist, and only then works out what the program should write. It ran here and passed all 302 of its checks. On 29 October 2026 we compiled and ran the program itself with GnuCOBOL 3.2: its console output, errors.txt and accepted.dat matched the expected output below line for line, and it ended with return code 4.

The feed and its rules

The copybook, /code/cobol-input-validation-class-inspect-test-numval.cpy, describes an 80-byte record:

Table 5.3-1
Field Bytes PICTURE Rule
In-Account 1 to 8 X(8) eight digits, zero-filled
In-Name 9 to 32 X(24) present; starts with a letter; letters, spaces, apostrophes, hyphens and periods only
In-Currency 33 to 35 X(3) three capital letters, and one of USD, EUR or GBP
In-Amount 36 to 47 X(12) a number NUMVAL accepts, at most two decimals, from 0.01 to 50,000.00
In-Value-Date-X 48 to 55 X(8), redefined as 9(8) a real date, YYYYMMDD
In-Type 56 X P for a payment, R for a refund
In-Reference 57 to 72 X(16) capitals, digits and hyphens, no spaces; required for a refund
FILLER 73 to 80 X(8) unused

Every field the sender fills in is PIC X, and the date also has a PIC 9(8) view through REDEFINES. That is on purpose. A field described as PIC 9 promises every later statement that it holds digits, and an edit program exists because nobody has checked that yet. The amount arrives as text with a decimal point in it, not as digits with an implied decimal point.

The data file, /datasets/cobol-input-validation-class-inspect-test-numval.dat, holds 24 records built so that each of the program's 19 rules fails at least once; six records are clean. Record 20 is 81 bytes long, because its name has 25 characters and pushes every later field one byte to the right. Record 21 has eight X'00' bytes, LOW-VALUES, where the account number should be.

Every field, every error

*> 04, 06 and 09 are successful reads, so AT END was not taken.
Validate-Record.
    MOVE 0 TO WS-Rec-Errors
    EVALUATE WS-In-Status
        WHEN "00"
            PERFORM Check-Account
            PERFORM Check-Name
            PERFORM Check-Currency
            PERFORM Check-Amount
            PERFORM Check-Date
            PERFORM Check-Type
            PERFORM Check-Reference
        WHEN "04"
        WHEN "06"
            MOVE 1 TO WS-Err-No
            MOVE 0 TO WS-Err-Len
            PERFORM Add-Error
        WHEN "09"
            MOVE 2 TO WS-Err-No
            MOVE 0 TO WS-Err-Len
            PERFORM Add-Error
    END-EVALUATE
    IF WS-Rec-Errors = 0
        PERFORM Write-Good
    ELSE
        ADD 1 TO WS-Rejected
    END-IF.
Listing 5.3-1. Lines 194 to 220 of cobol-input-validation.cob.

Validate-Record performs one paragraph per field, in record order, and all seven run for every record that was read cleanly. Inside each paragraph the rules are ordered and only the first one that fails is reported, so a date that is not numeric is not also tested as a calendar date, and a currency code that is not three capitals is not also looked up in the list. One error per field and every field per record tells the sender everything that is wrong without burying a cause under its consequences. Record 4 shows the effect: its account has a letter in it, its name has a zero where an O belongs, and its type is C, so it gets three error lines.

Each check paragraph moves the field it tests into WS-Err-Value, so that Add-Error can print the field between brackets after the message, leading and trailing spaces included. The messages sit in a table built the traditional way, nineteen FILLER items with VALUE clauses redefined as an OCCURS table; REDEFINES and OCCURS each have a lesson of their own. These are the last lines of it:

    05  FILLER PIC X(28) VALUE "REFERENCE HAS BAD CHARACTER".
01  Error-Table REDEFINES Error-Texts.
    05  Err-Text                PIC X(28) OCCURS 19 TIMES.
01  Code-Counts.
    05  Code-Count              PIC 9(4) OCCURS 19 TIMES.
Listing 5.3-2. Lines 114 to 118 of cobol-input-validation.cob.

The code itself is not stored anywhere. Add-Error prints "E" and the entry number, so a code and its message cannot drift apart. The nineteen counters beside the table, cleared by INITIALIZE when the files open, are what the summary prints.

Table 5.3-2
Code Message Raised when
E01 RECORD LONGER THAN 80 BYTES READ returned status 06, or 04
E02 CONTROL CHARACTER IN RECORD READ returned status 09
E03 ACCOUNT NOT NUMERIC not eight digits, and not digits after leading spaces either
E04 ACCOUNT NOT ZERO-FILLED leading spaces, then digits
E05 NAME MISSING the name is all spaces
E06 NAME HAS A BAD CHARACTER the first character is not a letter, or a character is outside Name-Char
E07 CURRENCY NOT 3 CAPITALS a character outside the class Capital
E08 CURRENCY NOT ACCEPTED not USD, EUR or GBP
E09 AMOUNT NOT A NUMBER TEST-NUMVAL is not zero; its position follows "at"
E10 AMOUNT HAS OVER 2 DECIMALS cutting the value to two decimals changes it
E11 AMOUNT NOT 0.01 TO 50000.00 out of range, or too big for the work field
E12 DATE NOT NUMERIC not eight digits
E13 DATE YEAR NOT 1601-9999 TEST-DATE-YYYYMMDD returned 1
E14 DATE MONTH NOT 01-12 it returned 2
E15 DATE DAY NOT IN MONTH it returned 3
E16 TYPE NOT P OR R the type is neither
E17 REFUND WITHOUT REFERENCE type R with a blank reference
E18 REFERENCE HAS A SPACE a space before the end of the reference
E19 REFERENCE HAS BAD CHARACTER a character outside Ref-Char

Classes of your own

SPECIAL-NAMES.
*> Letters as three runs, so the classes mean the same on EBCDIC
    CLASS Capital   IS "A" THRU "I" "J" THRU "R" "S" THRU "Z"
    CLASS Letter    IS "A" THRU "I" "J" THRU "R" "S" THRU "Z"
                       "a" THRU "i" "j" THRU "r" "s" THRU "z"
    CLASS Name-Char IS "A" THRU "I" "J" THRU "R" "S" THRU "Z"
                       "a" THRU "i" "j" THRU "r" "s" THRU "z"
                       " " "'" "-" "."
    CLASS Ref-Char  IS "A" THRU "I" "J" THRU "R" "S" THRU "Z"
                       "0" THRU "9" "-".
Listing 5.3-3. Lines 21 to 30 of cobol-input-validation.cob.

The built-in character classes that matter here are NUMERIC, ALPHABETIC, ALPHABETIC-LOWER and ALPHABETIC-UPPER, and the alphabetic ones are looser than their names. The guide's section on class conditions says they accept spaces as well as letters, and IBM's reference describes ALPHABETIC-UPPER as "the uppercase Latin alphabetic characters A through Z and the space" (page 270). GnuCOBOL 3.2's runtime test for it, cob_is_upper in libcob/common.c, passes any byte that is a space or an upper-case letter. So the currency code US passes ALPHABETIC-UPPER. Record 9 carries exactly that code, and the program rejects it with Capital, a class of its own that leaves the space out.

The CLASS clause in SPECIAL-NAMES names a set of characters, and a test against the class is true only when every character of the item is in the set. The rule is plain in the C that cobc writes for each class: a function that walks the field byte by byte and returns false at the first byte outside the set (output_class_name_definition in cobc/codegen.c). Two things in the listing follow from it. Name-Char has to include the space, or every name shorter than 24 characters would fail on its own padding; and because it includes the space, it would also pass a name that starts with one, so Check-Name tests the first character on its own against Letter. Ref-Char leaves the space out, so the program applies it only to the part of the reference before the padding, which INSPECT measures in the next section.

Check-Name.
    MOVE In-Name TO WS-Err-Value
    MOVE LENGTH OF In-Name TO WS-Err-Len
    EVALUATE TRUE
        WHEN In-Name = SPACES
            MOVE 5 TO WS-Err-No
            PERFORM Add-Error
        WHEN In-Name (1:1) IS NOT Letter
          OR In-Name IS NOT Name-Char
            MOVE 6 TO WS-Err-No
            PERFORM Add-Error
    END-EVALUATE.

Check-Currency.
    MOVE In-Currency TO WS-Err-Value
    MOVE LENGTH OF In-Currency TO WS-Err-Len
    EVALUATE TRUE
        WHEN In-Currency IS NOT Capital
            MOVE 7 TO WS-Err-No
            PERFORM Add-Error
        WHEN NOT Currency-Accepted
            MOVE 8 TO WS-Err-No
            PERFORM Add-Error
    END-EVALUATE.
Listing 5.3-4. Lines 240 to 263 of cobol-input-validation.cob.

The letters are written as three runs, A to I, J to R and S to Z, for a reason that has nothing to do with GnuCOBOL. THRU names every character from the first literal to the second; in IBM's words on page 129, the class "includes those characters that begin with the value of literal-4 and that end with the value of literal-5". In ASCII the 26 capitals are consecutive codes. In EBCDIC they are not: Table 81 of IBM's reference puts A to I at X'C1' to X'C9', J to R at X'D1' to X'D9' and S to Z at X'E2' to X'E9' (pages 751 to 754). On z/OS, "A" THRU "Z" therefore covers 41 code points, the 26 capitals and 15 others, among them the closing brace at X'D0' and the backslash at X'E0', and "a" THRU "z" takes in the euro sign at X'9F' and the tilde at X'A1'. Three runs give the same 26 letters in both codes and cost nothing in ASCII.

IBM also limits where a class of your own may be tested. Table 21 on page 270 allows a class-name test on alphabetic, alphanumeric and edited items, and gives an external decimal item, a PIC 9 field in USAGE DISPLAY, only NUMERIC. Every class test in this program is on a PIC X field, which both compilers accept.

INSPECT TALLYING: why a test failed

Check-Account.
    MOVE In-Account TO WS-Err-Value
    MOVE LENGTH OF In-Account TO WS-Err-Len
    IF In-Account IS NOT NUMERIC
        MOVE 0 TO WS-Lead-Spaces
        INSPECT In-Account TALLYING WS-Lead-Spaces FOR LEADING SPACES
        EVALUATE TRUE
            WHEN WS-Lead-Spaces = 0
              OR WS-Lead-Spaces = LENGTH OF In-Account
                MOVE 3 TO WS-Err-No
            WHEN In-Account (WS-Lead-Spaces + 1:) IS NUMERIC
                MOVE 4 TO WS-Err-No
            WHEN OTHER
                MOVE 3 TO WS-Err-No
        END-EVALUATE
        PERFORM Add-Error
    END-IF.
Listing 5.3-5. Lines 222 to 238 of cobol-input-validation.cob.

NUMERIC fails for 42178 and for 1000A219 alike, but they are different mistakes. The first is a number that was right-justified instead of zero-filled; the second has a letter in it. INSPECT TALLYING with FOR LEADING SPACES counts the spaces at the front of the field, three here, and if the bytes after them pass NUMERIC, the record gets E04 instead of E03. The reference modification In-Account (WS-Lead-Spaces + 1:) looks at those bytes only. IBM's reference requires the start of a reference modification to fall inside the field (page 75), so the EVALUATE deals with no spaces and with eight spaces first; its WHEN phrases are tried in order, and the reference modification is reached only when it is valid. An edit program could repair the first case with INSPECT REPLACING LEADING SPACES BY ZEROS. This one reports it, because an account number is a key and a repaired key is a guess about what the sender meant.

Check-Reference.
    IF In-Reference NOT = SPACES
        MOVE In-Reference TO WS-Err-Value
        MOVE LENGTH OF In-Reference TO WS-Err-Len
        MOVE 0 TO WS-Ref-Len WS-Ref-Spaces
        INSPECT In-Reference TALLYING
            WS-Ref-Len    FOR CHARACTERS BEFORE INITIAL SPACE
            WS-Ref-Spaces FOR ALL SPACES
        EVALUATE TRUE
            WHEN WS-Ref-Len = 0
              OR WS-Ref-Len + WS-Ref-Spaces < LENGTH OF In-Reference
                MOVE 18 TO WS-Err-No
                PERFORM Add-Error
            WHEN In-Reference (1:WS-Ref-Len) IS NOT Ref-Char
                MOVE 19 TO WS-Err-No
                PERFORM Add-Error
        END-EVALUATE
    END-IF.
Listing 5.3-6. Lines 320 to 337 of cobol-input-validation.cob.

The reference check runs one INSPECT with two counters. WS-Ref-Len FOR CHARACTERS BEFORE INITIAL SPACE counts every character up to the first space, which is the length of a well-formed reference. WS-Ref-Spaces FOR ALL SPACES counts the spaces. In a good reference the two add up to the field's 16 bytes, because everything after the first space is padding. INV 2026 77 gives 3 and 7, which make 10, so something other than a space follows the first space, and record 11 gets E18. INV#5521 gives 8 and 8, passes that test, and the class test on In-Reference (1:8) finds the number sign (E19).

Two rules of INSPECT matter here. The first causes real bugs: TALLYING adds to its counter and never clears it. The guide's INSPECT section says the statement "will not zero-out identifier-2 at the start of execution of the INSPECT", IBM's reference says "You must initialize identifier-2 before execution of the INSPECT statement begins" (page 356), and the 3.2 runtime adds each count to the field it is given (cob_add_int in libcob/strings.c). Without the MOVE 0 statements, every count would start from whatever the previous INSPECT left in the field. The second rule covers several counters in one statement: a position counted by an earlier phrase is not counted again by a later one. The guide's example runs C-ABC FOR ALL "ABC" and C-BCDE FOR ALL "BCDE" over --ABCDEF----BCDEF-- and ends with 1 in each, because the first BCDE overlaps an ABC that was already counted. In this program the two phrases cannot compete, since CHARACTERS stops at the first space and ALL SPACES counts nothing else.

The guide's INSPECT syntax also offers TRAILING, which would count the padding directly. GnuCOBOL's own test suite files TRAILING under extensions, and IBM's TALLYING format has only CHARACTERS, ALL and LEADING (page 353), so the listing does without it.

Amounts: TEST-NUMVAL before NUMVAL

Check-Amount.
    MOVE In-Amount TO WS-Err-Value
    MOVE LENGTH OF In-Amount TO WS-Err-Len
    MOVE FUNCTION TEST-NUMVAL (In-Amount) TO WS-Numval-Pos
    IF WS-Numval-Pos NOT = 0
        MOVE 9 TO WS-Err-No
        MOVE WS-Numval-Pos TO WS-Err-Pos
        PERFORM Add-Error
    ELSE
        COMPUTE WS-Amount-Wide = FUNCTION NUMVAL (In-Amount)
            ON SIZE ERROR
                MOVE 11 TO WS-Err-No
                PERFORM Add-Error
            NOT ON SIZE ERROR
                MOVE WS-Amount-Wide TO WS-Amount
                EVALUATE TRUE
                    WHEN WS-Amount NOT = WS-Amount-Wide
                        MOVE 10 TO WS-Err-No
                        PERFORM Add-Error
                    WHEN WS-Amount < 0.01 OR WS-Amount > 50000.00
                        MOVE 11 TO WS-Err-No
                        PERFORM Add-Error
                END-EVALUATE
        END-COMPUTE
    END-IF.
Listing 5.3-7. Lines 265 to 289 of cobol-input-validation.cob.

TEST-NUMVAL answers one question: would NUMVAL accept this text? It returns zero if so; otherwise it returns the position of the first character in error, or the length plus one when the text ends before it is complete (guide 8.1.89; IBM page 651). The program prints that position after "at". 1,250.75 gives 2, the comma: IBM's NUMVAL formats on page 605 allow a comma only as the decimal point under DECIMAL-POINT IS COMMA, and in the 3.2 runtime only NUMVAL-C accepts a grouping comma. The guide's NUMVAL section calls the comma a grouping separator; the 3.2 NUMVAL skips it and raises an exception. $45.00 gives 1, and 12.5.0 gives 5, the second point. The companion's copy of the routine reproduces all 32 expected results in GnuCOBOL's TEST-NUMVAL test, and IBM's example that 0 1 returns 3.

Why test first, when NUMVAL could complain for itself? Because in 3.2 it does not refuse anything. The NEWS file for the release says that when the argument breaks the rules, "previous versions returned zero; now invalid data is skipped; for example: "1. A-0B4.5" now returns -1.045". The function sets the EC-ARGUMENT-FUNCTION exception, which a program has to look for itself; the same NEWS entry names checking it afterwards, or calling TEST-NUMVAL first, as the ways to catch bad input. Run on the three amounts TEST-NUMVAL refused, the companion's copy of 3.2's NUMVAL returns 1250.75, 45.00 and 12.50. Two of those are probably what the sender meant. The third is not, and nothing in the value says which is which.

Once the text passes, COMPUTE converts it into WS-Amount-Wide, PIC S9(7)V9(11), with ON SIZE ERROR, which the lesson on COMPUTE covers. Eleven decimal places hold every decimal a 12-byte field can carry, so the conversion loses nothing, and seven integer places hold anything up to the limit and well past it. Record 23's 100000000.00 overflows them, which is E11. The MOVE to WS-Amount, PIC S9(7)V99, then drops every decimal after the second, as the MOVE rules say it must, and if the two fields now differ, the text had more than two decimals: 19.999 becomes 19.99 and gets E10. The range test comes last. 0.00 and 60000 fail it, and so does 125.00-: a trailing minus is valid NUMVAL syntax, so TEST-NUMVAL passes it and NUMVAL makes it -125.00.

The comparison relies on NUMVAL producing an exact decimal, which GnuCOBOL's does: the 3.2 routine collects the digits as text and builds the number with the GMP library (numval in libcob/intrinsic.c). IBM's does not. Its Programming Guide says NUMVAL returns "long (64-bit) floating-point values in compatibility mode" and that "At most 15 decimal digits can be converted accurately" (page 115), and a floating-point value moved to a fixed-point field is rounded in its low-order position (page 52). We have not worked out how the comparison behaves under that conversion.

Dates: TEST-DATE-YYYYMMDD

Check-Date.
    MOVE In-Value-Date-X TO WS-Err-Value
    MOVE LENGTH OF In-Value-Date-X TO WS-Err-Len
    IF In-Value-Date IS NOT NUMERIC
        MOVE 12 TO WS-Err-No
        PERFORM Add-Error
    ELSE
        MOVE FUNCTION TEST-DATE-YYYYMMDD (In-Value-Date)
            TO WS-Date-Result
        IF WS-Date-Result NOT = 0
            COMPUTE WS-Err-No = 12 + WS-Date-Result
            PERFORM Add-Error
        END-IF
    END-IF.
Listing 5.3-8. Lines 291 to 304 of cobol-input-validation.cob.

TEST-DATE-YYYYMMDD returns 0 for a valid date, and 1, 2 or 3 when the year, the month or the day is wrong. A valid year is 1601 to 9999, and the day is checked against the length of the month in that year, leap years included; the guide's section 8.1.86 and IBM's page 645 give the same codes. Adding 12 to the result picks E13, E14 or E15, so the error line says which part of the date is wrong. 20261399 returns 2: it is the date the bank-statement job let through. 20250229 returns 3, because 2025 is not a leap year, while record 19's 20240229 passes. 00000000 returns 1. The companion checks its copy against GnuCOBOL's own test, in which 20020231 returns 3, and against IBM's five examples.

The NUMERIC test comes first because the argument has to be an integer: IBM says "Must be an integer", and the guide asks for "a numeric integer data item or literal". 2026OCT1 stops there with E12. Leaving the test out would not produce an error in its place. The 3.2 NEWS file says that for invalid numeric DISPLAY data "only the second half-byte is used", so that a space and a zero both count as zero, and the routine that hands the function its argument, cob_display_get_int in libcob/move.c, works exactly that way. A date keyed as 2026 101, with a space for a zero, would reach TEST-DATE-YYYYMMDD as 20260101 and pass.

What READ has already decided

*> GnuCOBOL 3.2 returns a line longer than the record in pieces, the
*> first with status 06. Count lines, not reads, and drop the pieces.
Take-Record.
    IF NOT Read-Usable
        DISPLAY "Read failed after line " WS-Read
                ", status " WS-In-Status
        MOVE 12 TO RETURN-CODE
        STOP RUN
    END-IF
    IF In-Long-Line
        IF WS-In-Status NOT = "06"
            MOVE "N" TO WS-Long-Flag
        END-IF
    ELSE
        ADD 1 TO WS-Read
        IF WS-In-Status = "06"
            SET In-Long-Line TO TRUE
        END-IF
        PERFORM Validate-Record
    END-IF.
Listing 5.3-9. Lines 173 to 192 of cobol-input-validation.cob.

Two of the rules are settled before any field is looked at, by the way GnuCOBOL 3.2 reads LINE SEQUENTIAL files, and its NEWS file describes both. A character below the space, other than the tab, form feed, backspace, escape and shift-in that the runtime lets through, makes the READ return status 09, and a WRITE of such a character fails with status 71; the runtime option COB_LS_VALIDATE, on by default, controls the check. A line longer than the record is no longer cut quietly: "the data is returned as "multiple" records and a warning (status 06) is issued", and with COB_LS_SPLIT set to false the record is truncated with status 04 instead. Before 3.2, the NEWS file says, the data was cut and the status was 00.

Statuses 04, 06 and 09 all begin with 0, which makes them successful reads: the 3.2 runtime clears the exception code for any status in that class (save_status in libcob/fileio.c), so AT END is not taken and a program that tests only AT END carries on as if the record were normal. Take-Record looks at the status itself, in the FILE STATUS item that the sequential-file lesson introduced. Record 20 comes back as an 80-byte record with status 06, and a second read returns its last byte, a single space, padded to 80. Take-Record counts the line once, flags it E01 and drops the piece, so the record numbers in the error file stay equal to line numbers. Record 21 comes back with status 09 and gets E02. With COB_LS_SPLIT false, record 20 arrives once with status 04 and gets the same E01; the companion runs the file both ways and gets identical output.

Neither record is checked field by field. Every field of record 20 after the name is one byte off, and checking them would produce several errors about the wrong cause. Neither record is shown on its error line either: under the same default, writing the X'00' bytes of record 21 to a LINE SEQUENTIAL file would fail with status 71.

What the program writes

The companion's expected standard output:

Records read     :      24
Records accepted :       6
Records rejected :      18
Errors written   :      26
Accepted amount  :      53,966.44
Errors by code:
  E01      1  RECORD LONGER THAN 80 BYTES
  E02      1  CONTROL CHARACTER IN RECORD
  E03      1  ACCOUNT NOT NUMERIC
  E04      1  ACCOUNT NOT ZERO-FILLED
  E05      1  NAME MISSING
  E06      1  NAME HAS A BAD CHARACTER
  E07      2  CURRENCY NOT 3 CAPITALS
  E08      1  CURRENCY NOT ACCEPTED
  E09      3  AMOUNT NOT A NUMBER
  E10      1  AMOUNT HAS OVER 2 DECIMALS
  E11      4  AMOUNT NOT 0.01 TO 50000.00
  E12      1  DATE NOT NUMERIC
  E13      1  DATE YEAR NOT 1601-9999
  E14      1  DATE MONTH NOT 01-12
  E15      1  DATE DAY NOT IN MONTH
  E16      2  TYPE NOT P OR R
  E17      1  REFUND WITHOUT REFERENCE
  E18      1  REFERENCE HAS A SPACE
  E19      1  REFERENCE HAS BAD CHARACTER

And errors.txt, one line per error: the record number, the code, the message, the field between brackets and, for E09, the position TEST-NUMVAL reported.

     3  E04  ACCOUNT NOT ZERO-FILLED       [   42178]
     4  E03  ACCOUNT NOT NUMERIC           [1000A219]
     4  E06  NAME HAS A BAD CHARACTER      [J0hn Brook              ]
     4  E16  TYPE NOT P OR R               [C]
     5  E09  AMOUNT NOT A NUMBER           [1,250.75    ]             at  2
     5  E14  DATE MONTH NOT 01-12          [20261399]
     6  E07  CURRENCY NOT 3 CAPITALS       [usd]
     6  E10  AMOUNT HAS OVER 2 DECIMALS    [19.999      ]
     7  E11  AMOUNT NOT 0.01 TO 50000.00   [0.00        ]
     7  E15  DATE DAY NOT IN MONTH         [20250229]
     8  E08  CURRENCY NOT ACCEPTED         [JPY]
     8  E11  AMOUNT NOT 0.01 TO 50000.00   [60000       ]
     9  E07  CURRENCY NOT 3 CAPITALS       [US ]
     9  E16  TYPE NOT P OR R               [X]
    10  E17  REFUND WITHOUT REFERENCE      [                ]
    11  E18  REFERENCE HAS A SPACE         [INV 2026 77     ]
    12  E19  REFERENCE HAS BAD CHARACTER   [INV#5521        ]
    13  E05  NAME MISSING                  [                        ]
    13  E12  DATE NOT NUMERIC              [2026OCT1]
    14  E13  DATE YEAR NOT 1601-9999       [00000000]
    16  E11  AMOUNT NOT 0.01 TO 50000.00   [125.00-     ]
    17  E09  AMOUNT NOT A NUMBER           [$45.00      ]             at  1
    18  E09  AMOUNT NOT A NUMBER           [12.5.0      ]             at  5
    20  E01  RECORD LONGER THAN 80 BYTES
    21  E02  CONTROL CHARACTER IN RECORD
    23  E11  AMOUNT NOT 0.01 TO 50000.00   [100000000.00]

And accepted.dat, 67 bytes per record, with the amount as PIC 9(5)V99, so that 1,250.75 is written as 0125075:

10004217Ada Lovell              USD012507520261001PINV-2026-0042
10004218Mae O'Neill             EUR000752020260930RRF-88123
10004230Kofi Mensah-Bonsu Jr.   GBP499999920261005P
10004234Lucia Ortega            USD000005020240229PINV-2026-0055
10004237Chloe Martin            EUR020000020261007RRF-88125
10004239Eva Novak               USD006400020261008PINV-2026-0058

Three checks are worth doing by eye. Accepted and rejected add up to the 24 records read. The nineteen counts in the summary add up to the 26 error lines. And the six accepted amounts, 1,250.75, 75.20, 49,999.99, 0.50, 2,000.00 and 640.00, add up to 53,966.44. The third accepted line ends at its type, because record 15 has no reference and a LINE SEQUENTIAL WRITE drops trailing spaces by default (COB_LS_FIXED is false in the 3.2 runtime configuration). Because records were rejected, the program ends with RETURN-CODE 4; the guide's section on special registers describes RETURN-CODE as the item into which "a main program may MOVE a value before returning control to the operating system".

Compile and run

cobc -x -I code code/cobol-input-validation.cob
./cobol-input-validation
echo $?
python code/cobol-input-validation-class-inspect-test-numval.py errors.txt accepted.dat

Run the commands from the site folder, since the program opens its data file by a relative path, and -I code lets cobc find the copybook. echo $? should print 4. With the two file names, the companion compares the files the program wrote with its expected lines, ignoring trailing spaces, and prints any line that differs; with no arguments it prints the expected output and its own checks.

Where GnuCOBOL 3.2 and IBM differ

Table 5.3-3
Behaviour GnuCOBOL 3.2 (source and tests) IBM Enterprise COBOL 6.4 (page)
"A" THRU "Z" in a CLASS clause the 26 capitals (ASCII) 41 EBCDIC code points (129; Table 81, 751 to 754)
ALPHABETIC-UPPER capitals and the space capitals and the space (270)
INSPECT TALLYING FOR TRAILING an extension not in the format (353)
TEST-NUMVAL on 0 1 3 3 (651)
TEST-DATE-YYYYMMDD results 0, 1, 2 or 3 0, 1, 2 or 3 (645)
NUMVAL on text TEST-NUMVAL rejects skips the bad characters and sets EC-ARGUMENT-FUNCTION the argument must follow one of its formats (605)
Grouping comma in NUMVAL skipped, with the exception; NUMVAL-C accepts it not in either format (605)
NUMVAL result an exact decimal 64-bit floating point under ARITH(COMPAT), 128-bit under ARITH(EXTEND) (Programming Guide, 115)

Limitations

  • Compiled and run on one build. The expected output is the companion's, built from our reading of the 3.2 source, and a GnuCOBOL 3.2 build here reproduced it exactly on 29 October 2026. Its copies of TEST-NUMVAL, NUMVAL, TEST-DATE-YYYYMMDD, CLASS and INSPECT reproduce the expected results in GnuCOBOL's tests and IBM's examples; its copies of the LINE SEQUENTIAL read and of the numeric conversion rest on our reading of the source alone. If your GnuCOBOL build disagrees, the compiler wins, and the compare mode will show where.
  • Bytes, not characters. The classes test bytes. A name stored in UTF-8 with an accented letter has bytes outside Name-Char and would get E06, so a feed with such names needs a different rule, and its record length counts bytes too.
  • One error per field. A field with two faults shows only the first, and fixing it can reveal the second on the next run.
  • Constants in the code. The currency list and the amount range are written into the listing, and the record length into the copybook. A production edit keeps them in a control file or a table, so they can change without a recompile.
  • 3.2 defaults. The status handling follows the 3.2 defaults and COB_LS_SPLIT set to false. Under 3.1, record 20 would arrive with status 00 and every field after its name one byte off, and record 21's LOW-VALUES would reach the account check and get E03.
  • The IBM side. The z/OS behaviour comes from IBM's manuals; there is no z/OS system here to try any of it on.

Correction, 29 October 2026: the build command shown before named the source cobol-input-validation-class-inspect-test-numval.cob, and GnuCOBOL 3.2's cobc refuses a source file whose base name is longer than 31 characters ("invalid file base name ... length exceeds maximum"), even with -o, so it failed as printed. The listing is now shipped as cobol-input-validation.cob, and the old download address redirects there. The same day we compiled and ran the program with GnuCOBOL 3.2, and all three of its outputs matched the expected output shown here line for line, so the page now says that instead of calling the listing desk-checked only.

Index terms cobol, gnucobol, validation, inspect, test-numval