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.
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:
| 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.
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 "--"
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_inspectstops the compile with "CONVERTING operands differ in size", and the test suite's "INSPECT invalid size" case expects exactly that error forCONVERTING "ABC" TO "AA". A figurative constant is the exception:CONVERTING "ABC" TO SPACEScompiles 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 ".
Build-Key.
MOVE Ord-Name TO WS-Key
INSPECT WS-Key CONVERTING WS-Key-From TO WS-Key-To.
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.
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 "]".
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, soREPLACING ALL "BC" BY SPACEturnsBCAinto 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 instrings.csays 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"andALL "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-45would 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