COBOL Textbook

COBOL INSPECT REPLACING and CONVERTING, BEFORE and AFTER

A GnuCOBOL program that cleans an order export in place with INSPECT REPLACING, CONVERTING and BEFORE/AFTER, and the size and order rules behind them.

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

INSPECT is the statement COBOL uses to change characters inside a field without moving the field anywhere. It cannot make a field longer or shorter, and that one limit explains most of its rules. This lesson builds a program that cleans an order export in place: it zero-fills a quantity that was typed right-justified, turns slashes and dots in a date into hyphens, builds an upper-case key for matching names, lower-cases the domain of an e-mail address but not the part before the @, masks the address for printing, replaces a delimiter character typed into a free-text note, and upper-cases a voucher code between square brackets. Each step rests on one or two INSPECT statements, and each one shows a rule of REPLACING, CONVERTING, or the BEFORE and AFTER phrases that limit where they act.

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 and test suite of the GnuCOBOL 3.2 release, downloaded from the GNU project's server on October 15, 2026, where 3.2 is still the newest release listed. The expected output further down comes from the Python companion, /code/cobol-inspect-replacing-converting.py. It copies the INSPECT routines of the 3.2 runtime (libcob/strings.c) and the compiler's size check (cobc/typeck.c), passes 41 checks taken from GnuCOBOL's own test suite and from the worked examples in IBM's Language Reference and the guide, and then walks the data file the way the COBOL program does. On 29 October 2026 we compiled and ran the program with GnuCOBOL 3.2, and it printed exactly the output shown below and ended with return code 4.

The file

The copybook, /code/cobol-inspect-replacing-converting.cpy, describes a 100-byte order record. Every field is PIC X, so the program can repair a field before anything treats it as a number or a date:

Table 2.5-1
Field Size What arrives What the program does
Ord-Id X(6) an order number nothing
Ord-Name X(20) "Last, First" in any case builds a match key with CONVERTING
Ord-Date X(10) YYYY-MM-DD with /, . or - CONVERTING the separators to -
Ord-Qty X(4) a number, sometimes right-justified with spaces REPLACING LEADING SPACES BY ZEROS
Ord-Email X(36) an address in mixed case counts the @, CONVERTING after it, then a masked copy
Ord-Note X(24) free text, possibly with a vertical bar and a [code] TALLYING and REPLACING, then CONVERTING between the brackets

The data file, /datasets/cobol-inspect-replacing-converting.dat, has nine made-up orders. Four come out clean. The other five each break one rule: a blank quantity, a left-justified quantity, a day-first date, an address with no @ and an address with two. The addresses use example.com, example.org and example.net, the second-level names RFC 2606 reserves for documentation (section 3). Lines are written without their trailing spaces; a LINE SEQUENTIAL read pads a short line with spaces to the record size, as the STRING and UNSTRING lesson traced in the 3.2 file handler.

REPLACING LEADING: a quantity typed right-justified

Clean-Qty.
    MOVE Ord-Qty TO WS-Bad
    MOVE 4 TO WS-Bad-Len
    IF Ord-Qty = SPACES
        MOVE "QUANTITY IS BLANK" TO WS-Reason
    ELSE
        INSPECT Ord-Qty REPLACING LEADING SPACES BY ZEROS
        IF Ord-Qty IS NOT NUMERIC
            MOVE Ord-Qty TO WS-Bad
            MOVE "QUANTITY IS NOT A NUMBER" TO WS-Reason
        END-IF
    END-IF.
Listing 2.5-1. Lines 128 to 139 of cobol-inspect.cob.

A spreadsheet that right-aligns numbers exports 12, which fails the NUMERIC test because a space is not a digit. REPLACING LEADING SPACES BY ZEROS turns it into 0012. IBM's Language Reference defines LEADING by position: it replaces "each contiguous nonoverlapping occurrence" of the subject field, "provided that the leftmost such occurrence is at the point where comparison began" (page 357). In plain terms, LEADING works from the left edge and stops at the first character that does not match. Tanaka's quantity, 15, starts with a 1, so nothing is replaced, the trailing spaces stay, and the NUMERIC test rejects it. GnuCOBOL's runtime does the same thing in inspect_common_replacing: the LEADING loop breaks at the first position that does not match.

The test for SPACES comes first for a reason. On a blank field every character is a leading space, so the INSPECT would produce 0000, which passes NUMERIC and looks like a real order for zero items. A repair statement that turns missing data into valid data needs a guard in front of it.

The figurative constants are allowed on both sides. IBM's reference treats a figurative constant as a one-character literal (page 355), and GnuCOBOL's test suite runs the mirror image of this statement, REPLACING LEADING ZEROS BY SPACES on 0001, and expects 1.

CONVERTING: one character for another

Clean-Date.
    INSPECT Ord-Date CONVERTING "/." TO "--"
Listing 2.5-2. Lines 144 to 145 of cobol-inspect.cob.

CONVERTING takes two strings of equal length and swaps each character of the first for the character in the same position of the second, everywhere in the field. IBM describes it as a REPLACING with one ALL phrase per character (pages 358 and 359). In the 3.2 runtime it is simpler than that: cob_inspect_converting builds a 256-entry table from the two strings and passes over the field once, rewriting each byte that appears in the table. 2026/10/05 and 2026.10.06 both come out with hyphens.

Two rules from the sources matter here, and the sources do not quite agree on either.

  • Duplicates. IBM says the same character "must not appear more than once" in the first string (page 358). The 3.2 runtime fills its table "skipping duplicates", so the first occurrence wins. Leave duplicates out and the question never comes up.
  • Sizes. IBM requires the two strings to be the same size (page 358). The guide says that a shorter "to" string is padded with spaces and a longer one truncated. The 3.2 compiler does neither: its validate_inspect stops the compile with "CONVERTING operands differ in size", and the test suite's "INSPECT invalid size" case expects exactly that error for CONVERTING "ABC" TO "AA". A figurative constant is the exception: CONVERTING "ABC" TO SPACES compiles and turns all three letters into spaces.

CONVERTING changes characters and never moves them. Moreau's date, 15/10/2026, becomes 15-10-2026, which still has the day first, and the shape test that follows rejects it. Reordering a date takes reference modification or a numeric date function, not INSPECT.

A match key in one statement

01  WS-Key-From            PIC X(30)
        VALUE "abcdefghijklmnopqrstuvwxyz'.-,".
01  WS-Key-To              PIC X(30)
        VALUE "ABCDEFGHIJKLMNOPQRSTUVWXYZ    ".
Listing 2.5-3. Lines 50 to 53 of cobol-inspect.cob.
Build-Key.
    MOVE Ord-Name TO WS-Key
    INSPECT WS-Key CONVERTING WS-Key-From TO WS-Key-To.
Listing 2.5-4. Lines 199 to 201 of cobol-inspect.cob.

Names typed by people arrive as Okafor, Grace, o'neill-park, sian and de la Cruz, Ana. A key for matching them should not care about case or punctuation. One CONVERTING does both jobs, because the "to" string can hold spaces: the 26 lower-case letters map to capitals and the apostrophe, full stop, hyphen and comma map to the four spaces at the end of WS-Key-To. o'neill-park, sian becomes O NEILL PARK SIAN.

The two spaces before SIAN are the price of a statement that cannot change length: the comma became a space and the space after it is still there. INSPECT has no way to squeeze them out. The guide says as much in its INSPECT section, which sends anyone who needs a replacement of a different length to the SUBSTITUTE function. The route that works with any compiler is the one from the STRING and UNSTRING lesson: UNSTRING the key DELIMITED BY ALL SPACE into words and STRING them back together. For a match key the doubled space does no harm, as long as both sides of the match are built by the same statement.

FUNCTION UPPER-CASE (guide 8.1.93) would do the case half of this job. The guide notes that what counts as a letter for it can depend on a CHARACTER CLASSIFICATION; a CONVERTING string lists every character it touches, so its effect is visible in the source. The strings are kept in WORKING-STORAGE rather than written as literals because the program uses the alphabet in both directions, and IBM's reference makes one more demand that this layout satisfies: if either string shares storage with the field being inspected, "the result of the execution of this statement is undefined" (page 359).

AFTER INITIAL: the domain, not the local part

Clean-Email.
    MOVE Ord-Email TO WS-Bad
    MOVE 0 TO WS-Bad-Len
    INSPECT Ord-Email TALLYING WS-Bad-Len
        FOR CHARACTERS BEFORE INITIAL SPACE
    IF WS-Bad-Len = 0
        MOVE 1 TO WS-Bad-Len
    END-IF
    MOVE 0 TO WS-Ats
    INSPECT Ord-Email TALLYING WS-Ats FOR ALL "@"
    EVALUATE TRUE
        WHEN WS-Ats = 0
            MOVE "E-MAIL HAS NO @" TO WS-Reason
        WHEN WS-Ats > 1
            MOVE "E-MAIL HAS MORE THAN ONE @" TO WS-Reason
        WHEN Ord-Email (1:1) = "@"
            MOVE "E-MAIL HAS NOTHING BEFORE THE @" TO WS-Reason
        WHEN OTHER
            INSPECT Ord-Email CONVERTING WS-Upper TO WS-Lower
                AFTER INITIAL "@"
            MOVE Ord-Email TO WS-Mask
            INSPECT WS-Mask (2:) REPLACING CHARACTERS BY "*"
                BEFORE INITIAL "@"
    END-EVALUATE.
Listing 2.5-5. Lines 160 to 183 of cobol-inspect.cob.

Lower-casing a whole address is a common habit and, strictly, a mistake. RFC 5321, section 2.4, says the local part of a mailbox "MUST BE treated as case sensitive", while mailbox domains "follow normal DNS rules and are hence not case sensitive". The same section adds that relying on that case sensitivity "impedes interoperability and is discouraged", so most mail systems ignore it, but a cleaning program has no business deciding that for them. AFTER INITIAL "@" limits the CONVERTING to the characters after the first @: Grace.Okafor@EXAMPLE.COM becomes Grace.Okafor@example.com. The delimiter itself is never changed; IBM's reference says plainly that "delimiters are not counted or replaced" (page 357).

The @ is counted before anything is converted, because AFTER fails silently. IBM's rule for AFTER is that when the delimiter is not in the field, "no counting or replacement takes place" (page 358), and the 3.2 runtime does the same by setting the start of the range to its end (cob_inspect_after). Brennan's liam.brennan.example.com would pass through the CONVERTING untouched and unreported. The TALLYING finds zero @ signs and the record is rejected instead. Two @ signs would leave AFTER INITIAL choosing the first one, which is why Fitzgerald's anne@fitz@EXAMPLE.COM is rejected too. The first TALLYING, FOR CHARACTERS BEFORE INITIAL SPACE, measures the address for the reject message in the way the input validation lesson measured a reference number; both counters are set to zero first because TALLYING only ever adds.

BEFORE is the mirror image, with a different failure: when its delimiter is missing, IBM says replacing "continues toward the rightmost character position" (page 358). The masked copy uses that. WS-Mask (2:) starts the inspection at the second character, so the first one survives, and BEFORE INITIAL "@" stops it at the @. Every character in between becomes an asterisk: G***********@example.com. CHARACTERS BY takes exactly one character. IBM requires it (page 356), and the 3.2 compiler rejects a longer literal with "operand has wrong size" (cb_build_replacing_characters). De la Cruz's address, A@EXAMPLE.ORG, shows the edge case: the reference-modified field starts at the @, so the range before it is empty and nothing is masked.

TALLYING and REPLACING in one statement

Clean-Note.
    MOVE 0 TO WS-Bars
    INSPECT Ord-Note TALLYING WS-Bars FOR ALL "|"
        REPLACING ALL "|" BY "/"
    INSPECT Ord-Note CONVERTING WS-Lower TO WS-Upper
        AFTER INITIAL "[" BEFORE INITIAL "]".
Listing 2.5-6. Lines 189 to 194 of cobol-inspect.cob.

A program that writes vertical bars between fields cannot let a bar typed into a note through, so the first INSPECT counts the bars and replaces them in one statement. IBM defines this form, format 3, as if a TALLYING statement were "immediately followed by" a REPLACING statement (page 357), and its own example does the same with commas: more,perfect,union gives a count of 2 and more perfect union. The guide describes a single pass in which the counting runs first at each position, so the count reflects the field "before any replacements occur". The 3.2 parser takes IBM's route literally and emits two separate INSPECT sequences, the TALLYING one and then the REPLACING one. All three readings give the same count here: 1 for gift wrap|leave at door, 2 for fragile|[vip]|call ahead.

AFTER and BEFORE together

The second statement upper-cases a voucher code and nothing else in the note: voucher [spring-10] used becomes voucher [SPRING-10] used. With both phrases the range runs from just after the first [ to just before the first ]. IBM's own CONVERTING example has the same shape, upper-casing between a slash and a question mark: a/five/?six becomes a/FIVE/?six (Table 40, page 360). The two failure rules from the e-mail section combine. Van Dyke's note, code [autumn5, has no closing bracket, so BEFORE finds nothing and the range runs to the end of the field: code [AUTUMN5. A note with no opening bracket is left alone, whatever else it contains.

One thing the sources leave open is what happens when the BEFORE delimiter also appears ahead of the AFTER delimiter, as in ] see [x. In the 3.2 compiler the phrases run in the order they are written (inspect_region in parser.y), and cob_inspect_before searches from wherever the range currently starts, so AFTER INITIAL "[" BEFORE INITIAL "]" and BEFORE INITIAL "]" AFTER INITIAL "[" give different ranges on that text. A GnuCOBOL 3.2 run on 29 October 2026 bears that out: upper-casing ] see [x with AFTER written first gives ] see [X, because BEFORE then searches from just after the [ and finds no ], while BEFORE written first leaves the text unchanged. The IBM pages we read do not work through this case, so the data file never puts a ] in front of a [, and a program that might meet one should test for it with TALLYING first.

How REPLACING decides what to replace

Three rules govern a REPLACING phrase, and the 3.2 source shows where each lives.

  • Sizes must match. IBM: "The subject field and the substitution field must be the same length" (page 356). The guide again describes padding and truncation for a shorter or longer literal, and the 3.2 compiler again refuses: REPLACING ALL "ABC" BY "AA" is a compile error in the test suite. A figurative constant on the right repeats to fit, so REPLACING ALL "BC" BY SPACE turns BCA into two spaces and an A.
  • Comparisons see the original value. The 3.2 runtime writes each replacement into a separate buffer and copies them all back in cob_inspect_finish; a comment in strings.c says the original data "may not be changed to correctly handle multiple replacements with BEFORE/AFTER clauses". A delimiter that a phrase replaces is still found by the next phrase's BEFORE or AFTER.
  • A position is replaced once. IBM: after replacement has occurred in a character position, "no further replacement for that character position is made" (page 357). The runtime keeps a marker per byte for this. The guide's example runs ALL "ABC" BY "DEF" and ALL "BCDE" BY "WXYZ" over --ABCDEF----BCDEF-- and gets --DEFDEF----WXYZF--: the first BCDE overlaps an ABC that was already replaced, so only the second one changes.

There is also a difference in how the phrases take turns. IBM's comparison cycle (pages 360 and 361) stands at one position, tries every phrase in the order written, and moves on. The 3.2 runtime, as we read strings.c, runs each phrase over its whole range before the next phrase starts, with the markers keeping them apart. The two agree on every case in the companion's checks, including the guide's example above and the test suite's two-phrase CHARACTERS case, but they part ways when a later phrase matches earlier in the field than an overlapping match of an earlier phrase. For REPLACING ALL "B" BY "x" ALL "AB" BY "yy" on AB, IBM's cycle gives yy and our reading of the 3.2 code gives Ax; the companion prints both with --compare. A GnuCOBOL 3.2 run of that statement on 29 October 2026 printed Ax. We have not run it on IBM's compiler, so the yy is IBM's documented cycle, not a run. The program in this lesson sidesteps the question by giving every INSPECT a single REPLACING phrase.

The run

This is the output the companion works out for the data file, and the compiled program printed exactly these lines:

REC   1 100201 OK     OKAFOR  GRACE        2026-10-05 QTY 0012
        EMAIL  Grace.Okafor@example.com
        MASKED G***********@example.com
        NOTE   gift wrap/leave at door  (BARS REPLACED: 1)
REC   2 100202 OK     O NEILL PARK  SIAN   2026-10-06 QTY 0003
        EMAIL  sian@example.org
        MASKED s***@example.org
        NOTE   voucher [SPRING-10] used
REC   3 100203 OK     VAN DYKE  MARTA      2026-10-06 QTY 0040
        EMAIL  m.vandyke@example.net
        MASKED m********@example.net
        NOTE   code [AUTUMN5
REC   4 100204 REJECT QUANTITY IS BLANK [    ]
REC   5 100205 REJECT QUANTITY IS NOT A NUMBER [15  ]
REC   6 100206 REJECT DATE IS NOT YYYY-MM-DD [15-10-2026]
REC   7 100207 REJECT E-MAIL HAS NO @ [liam.brennan.example.com]
REC   8 100208 REJECT E-MAIL HAS MORE THAN ONE @ [anne@fitz@EXAMPLE.COM]
REC   9 100209 OK     DE LA CRUZ  ANA      2026-10-09 QTY 0005
        EMAIL  A@example.org
        MASKED A@example.org
        NOTE   fragile/[VIP]/call ahead  (BARS REPLACED: 2)
----
RECORDS READ    9
CLEANED         4
REJECTED        5

Nine records were read, four cleaned and five rejected. The rejected values are shown as the program left them: the date reject shows 15-10-2026 because the CONVERTING ran before the shape test. Because records were rejected, the program ends with return code 4.

Compile and run

From the site folder, with GnuCOBOL 3.x installed:

cobc -x -I code code/cobol-inspect.cob
./cobol-inspect
python code/cobol-inspect-replacing-converting.py
python code/cobol-inspect-replacing-converting.py --selftest
python code/cobol-inspect-replacing-converting.py --compare

The first two commands print the listing above; -I code lets the compiler find the copybook. The third prints the companion's version for comparison, the fourth lists its 41 checks, and the fifth prints the phrase-order case from the previous section. The program reads the data file by a relative path, so run it from the site folder.

Limitations

  • One compiler, one build. The listing was checked against the guide and the 3.2 source, the output above was worked out by the companion, and a GnuCOBOL 3.2 build reproduced it on 29 October 2026. Nothing here was run on IBM's compiler.
  • The date check is a shape check. It confirms four digits, a hyphen, two digits, a hyphen and two digits; 2026-13-45 would pass. The input validation lesson shows a calendar check with TEST-DATE-YYYYMMDD.
  • The address check is minimal. One @ with something before it is all the program requires. Spaces inside an address, a missing dot in the domain and everything else RFC 5321 allows or forbids are left alone.
  • ASCII letters only. The CONVERTING strings list the 26 letters of the English alphabet, and the data file is plain ASCII. An accented letter in a name is not converted, and a UTF-8 file stores such a letter in more than one byte, which INSPECT on a PIC X field treats as separate characters.
  • One phrase per statement. Every INSPECT here has one REPLACING phrase, so the phrase-order difference described above cannot change the output. A program that lists several phrases whose patterns can overlap should check the result on its own compiler.

The rule that runs through all of this is the one in the first paragraph: INSPECT keeps every field the same length. It can count, swap and overwrite characters, but it never inserts or removes one. A cleaning job that needs to remove characters, such as squeezing spaces or stripping punctuation out of a phone number, belongs to UNSTRING and STRING, and the useful habit is to notice which kind of change a field needs before reaching for either statement.

Correction, 29 October 2026: the build command shown before named the source cobol-inspect-replacing-converting.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-inspect.cob, and the old download address redirects there. The same day we compiled and ran the program with GnuCOBOL 3.2: its output matched the output shown here line for line, and the two cases this page had left for a compiler (BEFORE written ahead of AFTER, and two REPLACING phrases on AB) were run and their results added.

Index terms cobol, gnucobol, inspect, replacing, converting