Strings: STRING, UNSTRING and INSPECT

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. The ones presented as working compiled without warnings (also under -std=ibm), and the deliberately wrong ones show the real diagnostics. The examples use the Circulation library scenario of this section: member names and book titles from the copybooks MEMBREC and BOOKREC, and a CSV file of new books exported from a PC tool.

COBOL has no string type and no built-in split, replace or substring functions in the style of other languages. Text is a fixed-length PIC X(n) item, and the work is done by three statements and one notation:

  • reference modification, item(start:length), which names a part of an item;

  • STRING, which joins pieces into one item (concatenation);

  • UNSTRING, which splits one item into several (tokenising);

  • INSPECT, which counts, replaces or translates characters in place.

Section "Text intrinsic functions" adds TRIM, UPPER-CASE, LOWER-CASE, REVERSE and the GnuCOBOL functions CONCAT and SUBSTITUTE, and the last section puts the pieces together to parse a CSV line. The fixed-length nature of every item explains most of the pitfalls on this page: the receiving field is never shortened or cleared, trailing spaces are data, and a count of "characters" includes the padding. Moving whole items is on MOVE and Data Movement, the picture symbols of the items used here are on The PICTURE Clause, every intrinsic function is on Intrinsic Functions, and national and UTF-8 text is on National Data, UTF-8 and Code Pages.

Reference modification

Reference modification selects a substring of a data item by position. The notation is the item name followed by (leftmost-character-position : length), with a colon and no spaces needed. The length is optional: item(start:) runs to the end of the item.

data-name (start : [length])
FUNCTION function-name (arguments) (start : [length])

The rules, as the Enterprise COBOL Language Reference states them:

  • Positions count from 1. start must be a positive nonzero integer no greater than the number of characters in the item, and length, when given, must be a positive nonzero integer with start + length - 1 not beyond the end of the item. Both are arithmetic expressions, so BOOK-TITLE(WS-START + 4:WS-LEN) is valid.

  • The item can be qualified or subscripted; the reference modifier is evaluated right after the subscript, so WS-FIELD(WS-N)(WS-OUT-LEN:1) selects one character of one table element.

  • It works on an elementary item or on a group. For a numeric or edited DISPLAY item the characters are treated as if the item were alphanumeric, so WS-ID(3:2) on a PIC 9(6) item is the text 34 of 123456, not a number.

  • The result is an elementary item. It can be a sending field, a receiving field, an operand of STRING, UNSTRING and INSPECT, or the argument of a function. A reference-modified result of a function is allowed too: FUNCTION UPPER-CASE(WS-TITLE)(1:5).

  • For NATIONAL and DISPLAY-1 items the positions are character positions of two bytes each, not byte offsets.

The program below applies reference modification to the Circulation book record: a prefix of the ISBN, a computed window of the title, a target that only changes the named bytes, and a whole group.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. REFMOD1.
      * Reference modification on a BOOKMAST record.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
           COPY BOOKREC.
       01  WS-START             PIC 9(2) VALUE 1.
       01  WS-LEN               PIC 9(2) VALUE 4.
       01  WS-CODE              PIC X(3).
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE 123456 TO BOOK-ID
           MOVE "9780000000028" TO BOOK-ISBN
           MOVE "COBOL BASICS" TO BOOK-TITLE
           DISPLAY "ISBN prefix  : " BOOK-ISBN(1:3)
           DISPLAY "ISBN check   : " BOOK-ISBN(13:1)
           DISPLAY "Title tail   : [" BOOK-TITLE(6:) "]"
           DISPLAY "Computed     : [" BOOK-TITLE(WS-START:WS-LEN) "]"
           ADD 1 TO WS-START
           DISPLAY "Next window  : [" BOOK-TITLE(WS-START + 4:WS-LEN)
               "]"
           MOVE BOOK-TITLE(1:3) TO WS-CODE
           DISPLAY "Code         : " WS-CODE
      * As a target: only the named bytes change.
           MOVE "XYZ" TO BOOK-TITLE(1:3)
           DISPLAY "After MOVE   : [" BOOK-TITLE "]"
           MOVE ALL "-" TO BOOK-TITLE(13:)
           DISPLAY "After fill   : [" BOOK-TITLE "]"
      * A whole group can be reference-modified too.
           DISPLAY "Record 1:6   : " BOOK-RECORD(1:6)
           DISPLAY "Length group : " FUNCTION LENGTH(BOOK-RECORD)
           STOP RUN.
ISBN prefix  : 978
ISBN check   : 8
Title tail   : [ BASICS                  ]
Computed     : [COBO]
Next window  : [ BAS]
Code         : COB
After MOVE   : [XYZOL BASICS                  ]
After fill   : [XYZOL BASICS------------------]
Record 1:6   : 123456
Length group : 60

The compiler checks what it can at compile time. Constant positions and lengths that fall outside the item are errors, as in this deliberately wrong program (the source lines that cobc echoes are left out):

       IDENTIFICATION DIVISION.
       PROGRAM-ID. RMBAD1.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-TITLE             PIC X(10) VALUE "COBOL BASI".
       PROCEDURE DIVISION.
           DISPLAY WS-TITLE(11:1)
           DISPLAY WS-TITLE(0:1)
           DISPLAY WS-TITLE(5:9)
           STOP RUN.
rmbad1.cob:7: error: offset of 'WS-TITLE' out of bounds: 11
rmbad1.cob:8: error: offset of 'WS-TITLE' out of bounds: 0
rmbad1.cob:9: error: length of 'WS-TITLE' out of bounds: 9

When the position or the length is a variable, nothing is checked by default and the statement reads whatever follows the item in storage. The next program (also deliberately wrong) asks for six characters starting at position 8 of a 10-byte item. Compiled normally it displays three characters of the item followed by three unrelated bytes, which here are binary zeros (od -c shows [ A S I \0 \0 \0 ]). Compiled with -debug the same statement stops with a run-time error:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. RMBAD2.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-TITLE             PIC X(10) VALUE "COBOL BASI".
       01  WS-POS               PIC 9(2) VALUE 8.
       01  WS-LEN               PIC 9(2) VALUE 6.
       PROCEDURE DIVISION.
           DISPLAY "[" WS-TITLE(WS-POS:WS-LEN) "]"
           STOP RUN.
libcob: rmbad2.cob:9: error: length of 'WS-TITLE' out of bounds: 6, starting at: 8, maximum: 10

Enterprise COBOL gives the same protection with the SSRANGE compiler option (the Programming Guide section on checking valid ranges is covered on Tables and OCCURS). A zero length is invalid in Enterprise COBOL; in a test GnuCOBOL 3.2.0 accepted WS-TITLE(3:WS-LEN) with WS-LEN equal to 0 at run time, even with -debug, and produced an empty result, so do not rely on it.

STRING: joining data

STRING copies the leading part (or all) of one or more sending items into one receiving item, one after another, and can report when the receiver is too short.

STRING { identifier-1 | literal-1 } ...
       [ DELIMITED BY { identifier-2 | literal-2 | SIZE } ] ...
       INTO identifier-3
       [ WITH POINTER identifier-4 ]
       [ ON OVERFLOW imperative-statement-1 ]
       [ NOT ON OVERFLOW imperative-statement-2 ]
[ END-STRING ]

How it works, from the Enterprise COBOL Language Reference:

  • The sending items are processed left to right in the order written. Each DELIMITED BY applies to the sending items written before it (up to the previous DELIMITED BY). With DELIMITED BY SIZE the whole item is transferred. With a literal or identifier, characters are transferred up to, but not including, the first occurrence of the delimiter, or to the end of the item when it does not occur. The delimiter itself is not copied.

  • Transfer starts at the pointer position, which is 1 when there is no POINTER phrase. After each character is placed the pointer is increased by 1, so at the end it points one position past the last character transferred.

  • Only the positions that receive characters are changed. The rest of the receiver keeps its previous contents; it is not space filled. Clear it with MOVE SPACES first if you need a clean result.

  • The explicit pointer must be set to a value of at least 1 before the statement and, for a fixed-length receiver, no more than the receiver length. Declare it large enough for the length of the receiver plus 1.

  • Overflow occurs when the pointer is less than 1 or greater than the length of the receiver, whether at the start or while transferring. The transfer stops, NOT ON OVERFLOW is skipped, and control goes to the ON OVERFLOW statements if there are any. Without an ON OVERFLOW phrase the rest of the data is silently dropped.

  • Operand rules: numeric senders must be unsigned integers (no P in the picture); the receiver must not be numeric, edited or JUSTIFIED; all identifiers must have the same usage and all literals must match it. A figurative constant can be a delimiter but not one that begins with ALL. A sender and the receiver or the pointer must not overlap.

The program below builds a label with a literal delimiter of two spaces (the end of a name padded with spaces), shows the untouched tail of the receiver, appends in two steps with a pointer, and then makes the receiver too short.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. STRING1.
      * STRING: delimiters, POINTER and ON OVERFLOW in Circulation.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
           COPY MEMBREC.
           COPY BOOKREC.
       01  WS-LABEL             PIC X(40).
       01  WS-SHORT             PIC X(20).
       01  WS-PTR               PIC 9(3).
       01  WS-COPIES-OUT        PIC 9(3).
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE 101 TO MEMBER-ID
           MOVE "Grace Hopper" TO MEMBER-NAME
           MOVE "BATCH PROCESSING" TO BOOK-TITLE
           MOVE 1 TO BOOK-COPIES
      * 1. Delimited by a literal and by SIZE.
           MOVE ALL "*" TO WS-LABEL
           STRING MEMBER-NAME DELIMITED BY "  "
                  " borrowed " DELIMITED BY SIZE
                  BOOK-TITLE DELIMITED BY "  "
               INTO WS-LABEL
           END-STRING
           DISPLAY "1: [" WS-LABEL "]"
      * 2. The receiving field is not space filled: clear it first.
           MOVE SPACES TO WS-LABEL
           STRING MEMBER-ID DELIMITED BY SIZE
                  "/" DELIMITED BY SIZE
                  BOOK-COPIES DELIMITED BY SIZE
               INTO WS-LABEL
           END-STRING
           DISPLAY "2: [" WS-LABEL "]"
      * 3. WITH POINTER appends to what is already there.
           MOVE SPACES TO WS-LABEL
           MOVE 1 TO WS-PTR
           STRING "ID=" DELIMITED BY SIZE
                  MEMBER-ID DELIMITED BY SIZE
               INTO WS-LABEL WITH POINTER WS-PTR
           END-STRING
           DISPLAY "3a pointer now " WS-PTR
           STRING ";" DELIMITED BY SIZE
                  MEMBER-NAME DELIMITED BY "  "
               INTO WS-LABEL WITH POINTER WS-PTR
           END-STRING
           DISPLAY "3b pointer now " WS-PTR " [" WS-LABEL "]"
      * 4. ON OVERFLOW: the receiver is too short.
           MOVE SPACES TO WS-SHORT
           MOVE 1 TO WS-PTR
           STRING MEMBER-NAME DELIMITED BY "  "
                  " / " DELIMITED BY SIZE
                  BOOK-TITLE DELIMITED BY "  "
               INTO WS-SHORT WITH POINTER WS-PTR
               ON OVERFLOW
                   DISPLAY "4: overflow, pointer " WS-PTR
               NOT ON OVERFLOW
                   DISPLAY "4: fits"
           END-STRING
           DISPLAY "4: [" WS-SHORT "]"
      * 5. Without OVERFLOW the cut-off is silent.
           MOVE SPACES TO WS-SHORT
           STRING MEMBER-NAME DELIMITED BY "  "
                  " / " DELIMITED BY SIZE
                  BOOK-TITLE DELIMITED BY "  "
               INTO WS-SHORT
           END-STRING
           DISPLAY "5: [" WS-SHORT "]"
           STOP RUN.
1: [Grace Hopper borrowed BATCH PROCESSING**]
2: [000101/001                              ]
3a pointer now 010
3b pointer now 023 [ID=000101;Grace Hopper                  ]
4: overflow, pointer 021
4: [Grace Hopper / BATCH]
5: [Grace Hopper / BATCH]

Look at the output line by line:

  • Line 1 is Grace Hopper borrowed BATCH PROCESSING followed by **: the receiver was filled with asterisks first, and the two positions the statement did not reach keep the old contents.

  • Line 2 shows how a numeric item travels: MEMBER-ID (PIC 9(6), value 101) is copied as its six digits 000101, with leading zeros, and BOOK-COPIES as 001. The sender is treated as characters; move the number to a numeric-edited item first if the zeros are not wanted.

  • In 3a and 3b the pointer starts at 1, ends at 10 after the first statement (nine characters written) and at 23 after the second, so the second STRING appended after the first.

  • In 4 the receiver holds 20 characters. Grace Hopper / BATCH is exactly 20, so the rest of the title (` PROCESSING`) could not be placed: overflow was reported and the pointer stopped at 21, one past the end.

  • In 5 the same statement without ON OVERFLOW produced the same truncated text and no sign of the problem.

Two more behaviours of GnuCOBOL 3.2.0 are worth knowing, shown by the next program. A PIC 9(4)V99 sender (not an integer) is accepted and sent as its six digits without the decimal point, and a signed sender carries its sign in the last digit (an overpunch, here the character r for -12 in ASCII). An unset pointer of 0 is an overflow at once and nothing is transferred.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. STRING2.
      * STRING with numeric senders and an unset POINTER.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-PRICE             PIC 9(4)V99 VALUE 1250.
       01  WS-AMOUNT            PIC S9(4) VALUE -12.
       01  WS-OUT               PIC X(20).
       01  WS-PTR               PIC 9(3) VALUE ZERO.
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE ALL "*" TO WS-OUT
           STRING WS-PRICE DELIMITED BY SIZE INTO WS-OUT
           END-STRING
           DISPLAY "price  : [" WS-OUT "]"
           MOVE ALL "*" TO WS-OUT
           STRING WS-AMOUNT DELIMITED BY SIZE INTO WS-OUT
           END-STRING
           DISPLAY "signed : [" WS-OUT "]"
           MOVE ALL "*" TO WS-OUT
           STRING "abc" DELIMITED BY SIZE INTO WS-OUT
               WITH POINTER WS-PTR
               ON OVERFLOW
                   DISPLAY "pointer 0: overflow, pointer " WS-PTR
           END-STRING
           DISPLAY "unset  : [" WS-OUT "]"
           STOP RUN.
price  : [125000**************]
signed : [001r****************]
pointer 0: overflow, pointer 000
unset  : [********************]

GnuCOBOL accepts both numeric senders without a diagnostic, even under -std=ibm. Enterprise COBOL’s rule is stricter: a numeric sender must be an integer without the symbol P, so the first statement would not compile there. Move such a value to an edited item (for example PIC Z(3)9.99) first and send that item; the result is characters you chose.

UNSTRING: splitting data

UNSTRING is the opposite of STRING. It scans one sending item for delimiters and moves the pieces into several receiving items.

UNSTRING identifier-1
    [ DELIMITED BY [ ALL ] { identifier-2 | literal-1 }
                   [ OR [ ALL ] { identifier-3 | literal-2 } ] ... ]
    INTO { identifier-4 [ DELIMITER IN identifier-5 ]
                        [ COUNT IN identifier-6 ] } ...
    [ WITH POINTER identifier-7 ]
    [ TALLYING IN identifier-8 ]
    [ ON OVERFLOW imperative-statement-1 ]
    [ NOT ON OVERFLOW imperative-statement-2 ]
[ END-UNSTRING ]

The phrases, as the Language Reference defines them:

  • DELIMITED BY lists the delimiters. They can be literals or identifiers of one or more characters, joined with OR. A multi-character delimiter matches only when all its characters are contiguous and in order. With OR, the first delimiter that matches at the current position is used; the manual’s example is "AB" OR "BC", where ABC contains the delimiter AB.

  • ALL makes consecutive occurrences of a delimiter count as one. Without it, two delimiters in a row produce an empty receiving item (filled with spaces, or zeros for a numeric receiver).

  • Without DELIMITED BY, each receiver takes as many characters as its own size (for a numeric receiver the integer part of its size) and DELIMITER IN and COUNT IN are not allowed.

  • DELIMITER IN stores the delimiter that ended the piece (spaces when the piece ended at the end of the sending item). COUNT IN stores the number of characters examined for that piece, without the delimiter.

  • WITH POINTER starts scanning at the given character position and, when the statement ends, holds the initial value plus the number of positions examined. Initialise it to at least 1 yourself.

  • TALLYING IN adds the number of receiving items that were acted upon to the field, which you must initialise.

  • Overflow occurs when the pointer is outside the sending item, or when all receivers have been used and the sending item still has unexamined characters. Transfer stops and NOT ON OVERFLOW is skipped.

  • Each piece is moved by the normal MOVE rules for the sending category into the receiver, so a receiver that is too short gets the piece truncated on the right. A numeric receiver (usage DISPLAY or NATIONAL, no P) receives the piece with the MOVE rules for an alphanumeric sender, so the text must be digits; convert text such as 15.50 with FUNCTION NUMVAL instead.

  • A receiver that is not reached keeps its previous contents. Clear the receivers first.

The sending item must have category alphabetic, alphanumeric, alphanumeric-edited, DBCS, national, national-edited or UTF-8.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. UNSTR1.
      * UNSTRING: DELIMITER IN, COUNT IN, TALLYING, ALL and OR.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-LINE              PIC X(40).
       01  WS-F1                PIC X(12).
       01  WS-F2                PIC X(12).
       01  WS-F3                PIC X(12).
       01  WS-D1                PIC X(2).
       01  WS-D2                PIC X(2).
       01  WS-C1                PIC 9(2).
       01  WS-C2                PIC 9(2).
       01  WS-C3                PIC 9(2).
       01  WS-N                 PIC 9(2).
       01  WS-PTR               PIC 9(2).
       PROCEDURE DIVISION.
       MAIN-PARA.
      * 1. Fields, delimiters, counts and a tally.
           MOVE "100,Ada;Lovelace" TO WS-LINE
           MOVE ZERO TO WS-N
           MOVE SPACES TO WS-F1 WS-F2 WS-F3 WS-D1 WS-D2
           UNSTRING WS-LINE DELIMITED BY "," OR ";"
               INTO WS-F1 DELIMITER IN WS-D1 COUNT IN WS-C1
                    WS-F2 DELIMITER IN WS-D2 COUNT IN WS-C2
                    WS-F3 COUNT IN WS-C3
               TALLYING IN WS-N
           END-UNSTRING
           DISPLAY "1: [" WS-F1 "] [" WS-F2 "] [" WS-F3 "]"
           DISPLAY "   delimiters [" WS-D1 "] [" WS-D2 "]"
           DISPLAY "   counts " WS-C1 " " WS-C2 " " WS-C3
               "  tally " WS-N
      * 2. Two commas in a row give an empty field ...
           MOVE "A,,B" TO WS-LINE
           MOVE ZERO TO WS-N
           MOVE "xxxxxxxxxxxx" TO WS-F1 WS-F2 WS-F3
           UNSTRING WS-LINE DELIMITED BY ","
               INTO WS-F1 WS-F2 WS-F3
               TALLYING IN WS-N
           END-UNSTRING
           DISPLAY "2: [" WS-F1 "] [" WS-F2 "] [" WS-F3 "] " WS-N
      * 3. ... unless DELIMITED BY ALL folds them into one.
           MOVE ZERO TO WS-N
           MOVE "xxxxxxxxxxxx" TO WS-F1 WS-F2 WS-F3
           UNSTRING WS-LINE DELIMITED BY ALL ","
               INTO WS-F1 WS-F2 WS-F3
               TALLYING IN WS-N
           END-UNSTRING
           DISPLAY "3: [" WS-F1 "] [" WS-F2 "] [" WS-F3 "] " WS-N
      * 4. POINTER resumes where the last statement stopped.
           MOVE "ADA  GRACE  ALAN" TO WS-LINE
           MOVE 1 TO WS-PTR
           UNSTRING WS-LINE DELIMITED BY ALL SPACE
               INTO WS-F1 WITH POINTER WS-PTR
           END-UNSTRING
           DISPLAY "4a [" WS-F1 "] pointer " WS-PTR
           UNSTRING WS-LINE DELIMITED BY ALL SPACE
               INTO WS-F2 WITH POINTER WS-PTR
           END-UNSTRING
           DISPLAY "4b [" WS-F2 "] pointer " WS-PTR
      * 5. More fields in the data than receivers: ON OVERFLOW.
           MOVE "P,Q,R,S" TO WS-LINE
           UNSTRING WS-LINE DELIMITED BY ","
               INTO WS-F1 WS-F2 WS-F3
               ON OVERFLOW
                   DISPLAY "5: overflow, data left over"
               NOT ON OVERFLOW
                   DISPLAY "5: all consumed"
           END-UNSTRING
           STOP RUN.
1: [100         ] [Ada         ] [Lovelace    ]
   delimiters [, ] [; ]
   counts 03 03 32  tally 03
2: [A           ] [            ] [B           ] 03
3: [A           ] [B           ] [xxxxxxxxxxxx] 02
4a [ADA         ] pointer 06
4b [GRACE       ] pointer 13
5: overflow, data left over

Reading the output:

  • In 1 the delimiters were , and ;, recognised through OR, and DELIMITER IN returned each one in the PIC X(2) field (padded with a space). The last piece ended at the end of the 40-character WS-LINE, so its COUNT IN is 32: eight characters of Lovelace plus the 24 padding spaces. The receiver kept only its first 12 characters. A count that is larger than the receiver signals truncation, which the CSV program below uses.

  • In 2 the double comma gave an empty second field (spaces), and the tally is 3. In 3 DELIMITED BY ALL "," folded them into one, so the third receiver was never reached: it still holds the x characters it had, and the tally is 2.

  • In 4 the pointer is 6 after the first piece (ADA plus the two spaces folded by ALL SPACE) and 13 after the second. Calling UNSTRING repeatedly with a pointer is the way to walk through a line with a variable number of items.

  • In 5 there were four pieces for three receivers, so the overflow branch ran.

A pointer outside the sending item is an overflow as well. In a test, UNSTRING "A,B" ... WITH POINTER WS-PTR with WS-PTR equal to 0 took the ON OVERFLOW branch and left the pointer at 0.

INSPECT: counting, replacing and converting

INSPECT examines the characters of an item in place. It has four forms: tally, replace, both in one statement, and convert.

INSPECT identifier-1 TALLYING
    { identifier-2 FOR { CHARACTERS [ba] | { ALL | LEADING } { id | lit } [ba] } ... } ...

INSPECT identifier-1 REPLACING
    { CHARACTERS BY { id | lit } [ba]
    | { ALL | LEADING | FIRST } { id | lit } BY { id | lit } [ba] } ...

INSPECT identifier-1 TALLYING ... REPLACING ...

INSPECT identifier-1 CONVERTING { id | lit } TO { id | lit } [ba]

where [ba] is:  [ BEFORE INITIAL { id | lit } ]  [ AFTER INITIAL { id | lit } ]

The rules of the Language Reference that matter most:

  • TALLYING adds to the count field, which must be an integer item that you initialise; INSPECT never resets it. CHARACTERS counts every position (spaces included), ALL counts each nonoverlapping occurrence of a string, and LEADING counts occurrences only while they are unbroken from the start of the scanned region.

  • REPLACING substitutes the characters. The subject and substitution strings must have the same length, and CHARACTERS BY needs a one-character substitution. ALL replaces every nonoverlapping occurrence, LEADING only the unbroken run at the start, and FIRST only the leftmost one. Once a position is replaced it is not examined again in the same statement.

  • CONVERTING x TO y is a table of single-character replacements: the characters of x are translated to the characters at the same positions of y, which must be the same length. The same character must not appear twice in x.

  • BEFORE INITIAL and AFTER INITIAL restrict the area scanned: BEFORE stops at the first occurrence of the delimiter (or scans to the end if there is none), AFTER starts just past the first occurrence (and does nothing if there is none). The delimiters themselves are not counted or replaced. At most one BEFORE and one AFTER per phrase.

  • At each position the comparands are tried in the order written and the first match wins. When TALLYING and REPLACING are combined the statement works as the tally followed by the replacement.

  • Operands must be DISPLAY, DISPLAY-1 or NATIONAL of one and the same usage. A numeric DISPLAY item is examined as the characters of its digits (a signed one as an unsigned copy, with a separate sign byte left alone), so INSPECT can edit a numeric field, but the result is only meaningful if it still holds digits.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. INSP1.
      * INSPECT: TALLYING, REPLACING, CONVERTING, BEFORE and AFTER.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-TEXT              PIC X(30).
       01  WS-N                 PIC 9(3).
       01  WS-M                 PIC 9(3).
       01  WS-ISBN              PIC X(13) VALUE "978-0-00-0001".
       01  WS-AMT               PIC X(8)  VALUE "00001250".
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE "the old ledgers, old books" TO WS-TEXT
      * TALLYING: ALL, LEADING, CHARACTERS.
           MOVE ZERO TO WS-N
           INSPECT WS-TEXT TALLYING WS-N FOR ALL "old"
           DISPLAY "ALL old        : " WS-N
           MOVE ZERO TO WS-N
           INSPECT WS-TEXT TALLYING WS-N FOR ALL SPACE
           DISPLAY "ALL SPACE      : " WS-N
           MOVE ZERO TO WS-N
           INSPECT WS-TEXT TALLYING WS-N FOR CHARACTERS
               BEFORE INITIAL ","
           DISPLAY "CHARS BEFORE , : " WS-N
           MOVE ZERO TO WS-N
           INSPECT WS-AMT TALLYING WS-N FOR LEADING "0"
           DISPLAY "LEADING 0      : " WS-N
      * One statement, two counters.
           MOVE ZERO TO WS-N WS-M
           INSPECT WS-TEXT TALLYING WS-N FOR ALL "o"
                                   WS-M FOR ALL "k"
           DISPLAY "o and k        : " WS-N " " WS-M
      * REPLACING: ALL, FIRST, LEADING, CHARACTERS.
           INSPECT WS-TEXT REPLACING FIRST "old" BY "new"
           DISPLAY "FIRST          : [" WS-TEXT "]"
           INSPECT WS-TEXT REPLACING ALL "old" BY "new"
           DISPLAY "ALL            : [" WS-TEXT "]"
           INSPECT WS-AMT REPLACING LEADING "0" BY SPACE
           DISPLAY "LEADING        : [" WS-AMT "]"
           INSPECT WS-ISBN REPLACING ALL "-" BY SPACE
           DISPLAY "ISBN no hyphen : [" WS-ISBN "]"
      * BEFORE / AFTER limit the scan.
           MOVE "a-b-c-d" TO WS-TEXT
           INSPECT WS-TEXT REPLACING ALL "-" BY "+"
               AFTER INITIAL "b"
           DISPLAY "AFTER b        : [" WS-TEXT "]"
           MOVE "a-b-c-d" TO WS-TEXT
           INSPECT WS-TEXT REPLACING ALL "-" BY "+"
               BEFORE INITIAL "c"
           DISPLAY "BEFORE c       : [" WS-TEXT "]"
      * CONVERTING = a table of single-character replacements.
           MOVE "Dr. grace hopper" TO WS-TEXT
           INSPECT WS-TEXT CONVERTING
               "abcdefghijklmnopqrstuvwxyz"
               TO "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
           DISPLAY "CONVERTING     : [" WS-TEXT "]"
      * TALLYING and REPLACING in one statement.
           MOVE "x,y,z" TO WS-TEXT
           MOVE ZERO TO WS-N
           INSPECT WS-TEXT TALLYING WS-N FOR ALL ","
               REPLACING ALL "," BY ";"
           DISPLAY "both           : " WS-N " [" WS-TEXT "]"
      * The counter is added to, never reset by INSPECT.
           MOVE ZERO TO WS-N
           INSPECT WS-TEXT TALLYING WS-N FOR ALL ";"
           INSPECT WS-TEXT TALLYING WS-N FOR ALL "x"
           DISPLAY "accumulated    : " WS-N
      * The first matching comparand wins at each position.
           MOVE "ABC" TO WS-TEXT
           MOVE ZERO TO WS-N
           INSPECT WS-TEXT TALLYING WS-N FOR ALL "AB" ALL "BC"
           DISPLAY "AB then BC     : " WS-N
           STOP RUN.
ALL old        : 002
ALL SPACE      : 008
CHARS BEFORE , : 015
LEADING 0      : 004
o and k        : 004 001
FIRST          : [the new ledgers, old books    ]
ALL            : [the new ledgers, new books    ]
LEADING        : [    1250]
ISBN no hyphen : [978 0 00 0001]
AFTER b        : [a-b+c+d                       ]
BEFORE c       : [a+b+c-d                       ]
CONVERTING     : [DR. GRACE HOPPER              ]
both           : 002 [x;y;z                         ]
accumulated    : 003
AB then BC     : 001

A few results deserve a second look:

  • ALL SPACE counted 8, not 4: the item is 30 characters long and the text fills 26, so the four trailing pad spaces are counted with the four between words. CHARACTERS BEFORE INITIAL "," gave 15, the characters of the old ledgers.

  • LEADING "0" on 00001250 counted 4 and REPLACING LEADING "0" BY SPACE produced ` 1250`: a quick way to suppress zeros in a character field, although an edited picture (PIC Z(7)9) is the normal tool for numbers.

  • Two INSPECT statements on the same counter accumulate (2 plus 1 gave 3). In "AB" ... "BC" over ABC only the first comparand counted: once AB matched, the scan moved past B, and BC could no longer start there.

  • The BEFORE and AFTER phrases changed only part of a-b-c-d. CONVERTING upper-cased the whole name by translating 26 letters to 26 letters. FUNCTION UPPER-CASE does the same for plain text and is shorter, but CONVERTING can express any translation table, such as mapping accented letters to plain ones.

Operands of different sizes are rejected at compile time:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. BAD1.
      * Deliberately wrong: operands of different sizes.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-TEXT              PIC X(30) VALUE "the old ledgers".
       PROCEDURE DIVISION.
       MAIN-PARA.
           INSPECT WS-TEXT REPLACING ALL "old" BY "ne"
           INSPECT WS-TEXT CONVERTING "abc" TO "AB"
           STOP RUN.
bad1.cob:9: error: REPLACING operands differ in size
bad1.cob:10: error: CONVERTING operands differ in size

GnuCOBOL also accepts a TRAILING phrase in INSPECT TALLYING and REPLACING (INSPECT x TALLYING n FOR TRAILING SPACE gave 11 for the 9-character AA/B/C//D in a PIC X(20) item). It is not in the Enterprise COBOL 6.5 INSPECT formats, which offer only ALL, LEADING and FIRST. Where portability to z/OS matters, count the trailing spaces with FUNCTION LENGTH and FUNCTION TRIM instead.

INSPECT BACKWARD (COBOL 2023)

The COBOL 2023 standard adds a BACKWARD phrase to INSPECT, so that the scan runs from the right end of the item to the left. The use is to replace or count the last occurrence of something, for example the last / of a path, which the forward-only FIRST cannot do. Neither compiler in this section implements it:

  • Enterprise COBOL for z/OS 6.5 has no BACKWARD phrase in any of its INSPECT formats (the word does not appear in the Language Reference), so the same effect needs a reference-modification loop.

  • GnuCOBOL 3.2.0 lists BACKWARD as a reserved word but does not implement the phrase. In the default dialect the next program is a compile error, and the same under -std=ibm. Only under -std=mf does the parser accept INSPECT BACKWARD item, placed right after INSPECT, and then it warns that the phrase is not implemented and the statement runs forward.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. INSP2.
      * INSPECT ... BACKWARD (COBOL 2023) in GnuCOBOL 3.2.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-PATH              PIC X(20) VALUE "A/B/C/D".
       01  WS-N                 PIC 9(3).
       PROCEDURE DIVISION.
           MOVE ZERO TO WS-N
           INSPECT WS-PATH TALLYING WS-N FOR ALL "/"
           DISPLAY WS-N
           INSPECT BACKWARD WS-PATH REPLACING FIRST "/" BY "|"
           DISPLAY "[" WS-PATH "]"
           STOP RUN.

Default dialect (the same two errors appear with -std=ibm):

insp2.cob:12: error: 'BACKWARD' cannot be used here
insp2.cob:12: error: syntax error, unexpected Identifier, expecting CONVERTING or REPLACING or TALLYING

With -std=mf:

insp2.cob:13: warning: INSPECT BACKWARD is not implemented [-Wpending]

With -std=mf the replacement FIRST "/" BY "|" hit the first slash of A/B/C/D and printed A|B/C/D, where a real backward scan would give A/B/C|D. Until your compilers support the phrase, write the backward search by hand with reference modification, as in this program, which scans from the last non-space character towards the start:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. BACKWD.
      * The last "/" of a path, found with reference modification.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-PATH              PIC X(20) VALUE "A/B/C/D".
       01  WS-I                 PIC 9(3).
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE FUNCTION LENGTH(FUNCTION TRIM(WS-PATH TRAILING))
               TO WS-I
           PERFORM UNTIL WS-I = 0
               IF WS-PATH(WS-I:1) = "/"
                   MOVE "|" TO WS-PATH(WS-I:1)
                   EXIT PERFORM
               END-IF
               SUBTRACT 1 FROM WS-I
           END-PERFORM
           DISPLAY "[" WS-PATH "] last slash was at " WS-I
           STOP RUN.
[A/B/C|D             ] last slash was at 006

FUNCTION REVERSE is another common substitute: reverse the item, INSPECT ... FIRST, and reverse it back. The text of the standard is paywalled, so this page does not quote its wording of the phrase; see the ISO catalogue entry in the references.

Text intrinsic functions

Intrinsic functions return a value that is used where an item of that type could be. The text ones are the pieces that STRING, UNSTRING and INSPECT do not cover. The notation, FUNCTION name(arguments), and the whole function catalogue are on Intrinsic Functions.

Function Result Where available

TRIM(x), TRIM(x LEADING), TRIM(x TRAILING)

x without leading and trailing spaces, or without leading, or without trailing. An argument that is all spaces gives a zero-length result.

Enterprise COBOL 6.5, GnuCOBOL 3.2

UPPER-CASE(x), LOWER-CASE(x)

The text with letters converted to upper or lower case; other characters are unchanged.

Enterprise COBOL 6.5, GnuCOBOL 3.2

REVERSE(x)

The characters in reverse order.

Enterprise COBOL 6.5, GnuCOBOL 3.2

CONCAT(x, y, ...), CONCATENATE(...)

The arguments joined as they are, trailing spaces included.

GnuCOBOL 3.2 only (not in Enterprise COBOL 6.5)

SUBSTITUTE(x, find1, repl1, ...), SUBSTITUTE-CASE(...)

x with every occurrence of each find replaced by the matching repl. The -CASE form ignores case when matching.

GnuCOBOL 3.2 only (not in Enterprise COBOL 6.5)

LENGTH(x), BYTE-LENGTH(x)

Characters and bytes of an item, group or function result.

Enterprise COBOL 6.5, GnuCOBOL 3.2

NUMVAL(x), NUMVAL-C(x)

The number held in the text x (an optional sign, digits, a decimal point; NUMVAL-C also allows a currency sign and thousands separators).

Enterprise COBOL 6.5, GnuCOBOL 3.2

The first program applies the portable functions to a member name with leading spaces and mixed case. The function result can be sent to a DISPLAY, moved to an item, reference-modified, passed to STRING, or nested in another function.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. FUNC1.
      * Intrinsic functions for text, applied to Circulation data.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
           COPY MEMBREC.
       01  WS-OUT               PIC X(60).
       01  WS-BLANK             PIC X(5) VALUE SPACES.
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE "   grace HOPPER" TO MEMBER-NAME
           DISPLAY "TRIM        [" FUNCTION TRIM(MEMBER-NAME) "]"
           DISPLAY "TRIM LEAD   [" FUNCTION TRIM(MEMBER-NAME LEADING)
               "]"
           DISPLAY "TRIM TRAIL  [" FUNCTION TRIM(MEMBER-NAME TRAILING)
               "]"
           DISPLAY "UPPER-CASE  ["
               FUNCTION UPPER-CASE(FUNCTION TRIM(MEMBER-NAME)) "]"
           DISPLAY "LOWER-CASE  ["
               FUNCTION LOWER-CASE(FUNCTION TRIM(MEMBER-NAME)) "]"
           DISPLAY "REVERSE     ["
               FUNCTION REVERSE(FUNCTION TRIM(MEMBER-NAME)) "]"
           DISPLAY "LENGTH      "
               FUNCTION LENGTH(FUNCTION TRIM(MEMBER-NAME))
           DISPLAY "LENGTH blank "
               FUNCTION LENGTH(FUNCTION TRIM(WS-BLANK))
      * Receiving the result with MOVE and STRING.
           MOVE FUNCTION UPPER-CASE(MEMBER-NAME(4:5)) TO WS-OUT
           DISPLAY "MOVE        [" FUNCTION TRIM(WS-OUT) "]"
           MOVE SPACES TO WS-OUT
           STRING FUNCTION TRIM(MEMBER-NAME) DELIMITED BY SIZE
                  ", member" DELIMITED BY SIZE
               INTO WS-OUT
           END-STRING
           DISPLAY "STRING      [" FUNCTION TRIM(WS-OUT) "]"
           STOP RUN.
TRIM        [grace HOPPER]
TRIM LEAD   [grace HOPPER               ]
TRIM TRAIL  [   grace HOPPER]
UPPER-CASE  [GRACE HOPPER]
LOWER-CASE  [grace hopper]
REVERSE     [REPPOH ecarg]
LENGTH      000000012
LENGTH blank 000000000
MOVE        [GRACE]
STRING      [grace HOPPER, member]

Most of what people do with TRIM is in front of a STRING or a DISPLAY: STRING FUNCTION TRIM(MEMBER-NAME) DELIMITED BY SIZE does what DELIMITED BY " " did above without needing two spaces to be present. LENGTH of a trimmed item is the number of characters actually in use. For an all-space argument TRIM gives a zero-length result: the length printed as 000000000, which is also what the Enterprise COBOL 6.5 manual documents.

The second program uses the GnuCOBOL-only functions. CONCAT joins its arguments without removing any padding, so give it trimmed arguments when the items have trailing spaces; SUBSTITUTE takes any number of find/replace pairs.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. FUNC2.
      * GnuCOBOL extensions: CONCAT and SUBSTITUTE.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-ISBN              PIC X(13) VALUE "978-0-00-0001".
       01  WS-LINE              PIC X(30)
                                VALUE "Return of the old books".
       PROCEDURE DIVISION.
       MAIN-PARA.
           DISPLAY "CONCAT     ["
               FUNCTION CONCAT("ISBN:", WS-ISBN) "]"
           DISPLAY "SUBSTITUTE ["
               FUNCTION SUBSTITUTE(WS-ISBN, "-", "/") "]"
           DISPLAY "SUBST many ["
               FUNCTION TRIM(FUNCTION SUBSTITUTE(WS-LINE,
                   "old", "new", "Return", "Checkout")) "]"
           STOP RUN.
CONCAT     [ISBN:978-0-00-0001]
SUBSTITUTE [978/0/00/0001]
SUBST many [Checkout of the new books]

Enterprise COBOL 6.5 does not have these two (neither appears in the Language Reference), so code that has to run on z/OS builds the result with STRING and INSPECT REPLACING or INSPECT CONVERTING. GnuCOBOL also compiles them under -std=ibm, so the dialect option does not warn you. A replacement string of zero length looks like a way to delete characters, but it is not: replacing - by "" as in FUNCTION SUBSTITUTE(WS-ISBN, "-", "") compiled with this warning and the empty literal became a single space.

subs_bad.cob:14: warning: alphanumeric literal has zero length; a SPACE will be assumed [-Wothers]

To delete characters, use a nested FUNCTION TRIM, a STRING that skips them, or UNSTRING ... DELIMITED BY "-" followed by a STRING of the pieces.

Parsing a CSV line

Files from non-mainframe systems often arrive as comma separated values (CSV): one record per line, fields separated by commas, text in double quotes when it contains a comma or a quote, a header line, and CR LF or LF line ends. COBOL has no CSV reader, so the program does it with UNSTRING for simple lines and a character scanner when quoted fields can contain commas.

The next program loads the new books of a file books.csv. The first line is a header, which it skips. Each of the other lines is split with a single UNSTRING into five alphanumeric fields. COUNT IN measures the ISBN and the title as delivered, and TALLYING IN counts the fields. A line is accepted when it has exactly five fields, a 13-character ISBN and a title that fits the 30 characters of BOOK-TITLE. The numeric text is converted with FUNCTION NUMVAL, which tolerates leading zeros, a sign and a decimal point, so 15.50 reaches BOOK-PRICE (PIC 9(4)V99) correctly. The data file, written by a PC tool, has a CR LF line end on its third line, then one line with too many fields and one with too few:

id,isbn,title,copies,price
5,9780000000059,TAPE LIBRARIES,2,15.50
6,9780000000066,JOB SCHEDULING,1,22.00
7,9780000000073,TOO,MANY,FIELDS,9,1.00
8,9780000000080,SHORT ROW,3
       IDENTIFICATION DIVISION.
       PROGRAM-ID. CSV1.
      * Load BOOKREC records from a CSV exported by a PC tool.
       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT CSV-FILE ASSIGN TO "books.csv"
               ORGANIZATION IS LINE SEQUENTIAL.
       DATA DIVISION.
       FILE SECTION.
       FD  CSV-FILE.
       01  CSV-LINE             PIC X(100).
       WORKING-STORAGE SECTION.
           COPY BOOKREC.
       01  WS-EOF               PIC X VALUE "N".
           88  AT-END           VALUE "Y".
       01  WS-LINE-NO           PIC 9(3) VALUE ZERO.
       01  WS-FIELDS.
           05  WS-ID-X          PIC X(10).
           05  WS-ISBN-X        PIC X(20).
           05  WS-TITLE-X       PIC X(40).
           05  WS-COPIES-X      PIC X(10).
           05  WS-PRICE-X       PIC X(12).
       01  WS-COUNTS.
           05  WS-TITLE-LEN     PIC 9(3).
           05  WS-ISBN-LEN      PIC 9(3).
       01  WS-NFIELDS           PIC 9(2).
       01  WS-OK                PIC 9(2) VALUE ZERO.
       01  WS-BAD               PIC 9(2) VALUE ZERO.
       PROCEDURE DIVISION.
       MAIN-PARA.
           OPEN INPUT CSV-FILE
           READ CSV-FILE
               AT END SET AT-END TO TRUE
           END-READ
      * The first line is the header: skip it.
           PERFORM UNTIL AT-END
               READ CSV-FILE
                   AT END SET AT-END TO TRUE
                   NOT AT END
                       ADD 1 TO WS-LINE-NO
                       PERFORM PARSE-LINE
               END-READ
           END-PERFORM
           CLOSE CSV-FILE
           DISPLAY "loaded " WS-OK ", rejected " WS-BAD
           STOP RUN.
       PARSE-LINE.
           MOVE ZERO TO WS-NFIELDS WS-TITLE-LEN WS-ISBN-LEN
           MOVE SPACES TO WS-FIELDS
           UNSTRING CSV-LINE DELIMITED BY ","
               INTO WS-ID-X
                    WS-ISBN-X COUNT IN WS-ISBN-LEN
                    WS-TITLE-X COUNT IN WS-TITLE-LEN
                    WS-COPIES-X
                    WS-PRICE-X
               TALLYING IN WS-NFIELDS
               ON OVERFLOW
                   PERFORM REJECT-LINE
               NOT ON OVERFLOW
                   PERFORM CHECK-AND-STORE
           END-UNSTRING.
       CHECK-AND-STORE.
           IF WS-NFIELDS NOT = 5 OR WS-ISBN-LEN NOT = 13
              OR WS-TITLE-LEN > 30
               PERFORM REJECT-LINE
           ELSE
               MOVE FUNCTION NUMVAL(WS-ID-X) TO BOOK-ID
               MOVE WS-ISBN-X TO BOOK-ISBN
               MOVE WS-TITLE-X TO BOOK-TITLE
               MOVE FUNCTION NUMVAL(WS-COPIES-X) TO BOOK-COPIES
               MOVE FUNCTION NUMVAL(WS-PRICE-X) TO BOOK-PRICE
               SET BOOK-ACTIVE TO TRUE
               ADD 1 TO WS-OK
               DISPLAY "ok  " WS-LINE-NO " " BOOK-ID " " BOOK-TITLE
                   " " BOOK-COPIES " " BOOK-PRICE
           END-IF.
       REJECT-LINE.
           ADD 1 TO WS-BAD
           DISPLAY "bad " WS-LINE-NO " fields " WS-NFIELDS.
ok  001 000005 TAPE LIBRARIES                 002 0015.50
ok  002 000006 JOB SCHEDULING                 001 0022.00
bad 003 fields 05
bad 004 fields 04
loaded 02, rejected 02

Points to notice:

  • The long line has seven pieces for five receivers, so ON OVERFLOW rejects it. The short line has four pieces, the UNSTRING ends normally with a tally of 4, and the explicit tally check rejects it. Both checks are needed.

  • In GnuCOBOL 3.2.0 READ of a LINE SEQUENTIAL file removes the carriage return of a CR LF line end (a test with INSPECT ... TALLYING FOR ALL X"0D" counted none on line 3), which is why no cleaning step is needed. Do not assume the same elsewhere: a file transferred to z/OS in binary mode, or read as fixed-length records, keeps its X"0D" as the last character of the last field, and NUMVAL or a comparison can then fail. Convert the line ends (or the code page, see National Data, UTF-8 and Code Pages) when the file is uploaded, or INSPECT ... REPLACING ALL X"0D" BY SPACE before parsing. Enterprise COBOL supports LINE SEQUENTIAL only for z/OS UNIX files.

  • The 13-character check on COUNT IN is what catches an ISBN that was cut by the PIC X(20) receiver, or too short.

  • UNSTRING cannot tell a comma that separates fields from one inside quotes. The title "Ledgers, Old and New" would be split in two. The next program shows a scanner built on reference modification that handles quoted fields and the doubled-quote escape "", one character at a time.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. CSV2.
      * Split a CSV line that has quoted fields (UNSTRING cannot).
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-SAMPLES.
           05  FILLER  PIC X(60) VALUE
               '9,"Ledgers, Old and New",2'.
           05  FILLER  PIC X(60) VALUE
               '10,"The ""Batch"" Handbook",1'.
           05  FILLER  PIC X(60) VALUE
               '11,Plain Title,,4'.
       01  WS-SAMPLE-TABLE REDEFINES WS-SAMPLES.
           05  WS-SAMPLE        PIC X(60) OCCURS 3 TIMES.
       01  WS-IN                PIC X(60).
       01  WS-LEN               PIC 9(3).
       01  WS-I                 PIC 9(3).
       01  WS-N                 PIC 9(2).
       01  WS-K                 PIC 9(2).
       01  WS-OUT-LEN           PIC 9(3).
       01  WS-CH                PIC X.
           88  CH-QUOTE         VALUE '"'.
           88  CH-COMMA         VALUE ",".
       01  WS-NEXT              PIC X.
           88  NEXT-QUOTE       VALUE '"'.
       01  WS-QUOTED            PIC X.
           88  IN-QUOTES        VALUE "Y".
           88  NOT-IN-QUOTES    VALUE "N".
       01  WS-SKIP              PIC X.
           88  SKIP-NEXT        VALUE "Y".
           88  NO-SKIP          VALUE "N".
       01  WS-FIELD-TABLE.
           05  WS-FIELD         PIC X(30) OCCURS 6 TIMES.
       PROCEDURE DIVISION.
       MAIN-PARA.
           PERFORM VARYING WS-K FROM 1 BY 1 UNTIL WS-K > 3
               MOVE WS-SAMPLE(WS-K) TO WS-IN
               PERFORM SPLIT-LINE
               DISPLAY "line " WS-K ": " WS-N " fields"
               PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > WS-N
                   DISPLAY "  " WS-I " [" FUNCTION TRIM(WS-FIELD(WS-I))
                       "]"
               END-PERFORM
           END-PERFORM
           STOP RUN.
       SPLIT-LINE.
           MOVE SPACES TO WS-FIELD-TABLE
           MOVE FUNCTION LENGTH(FUNCTION TRIM(WS-IN TRAILING))
               TO WS-LEN
           MOVE 1 TO WS-N
           MOVE ZERO TO WS-OUT-LEN
           SET NOT-IN-QUOTES TO TRUE
           SET NO-SKIP TO TRUE
           PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > WS-LEN
               IF SKIP-NEXT
                   SET NO-SKIP TO TRUE
               ELSE
                   MOVE WS-IN(WS-I:1) TO WS-CH
                   MOVE SPACE TO WS-NEXT
                   IF WS-I < WS-LEN
                       MOVE WS-IN(WS-I + 1:1) TO WS-NEXT
                   END-IF
                   EVALUATE TRUE
                       WHEN CH-QUOTE AND IN-QUOTES AND NEXT-QUOTE
                           PERFORM APPEND-CHAR
                           SET SKIP-NEXT TO TRUE
                       WHEN CH-QUOTE AND IN-QUOTES
                           SET NOT-IN-QUOTES TO TRUE
                       WHEN CH-QUOTE
                           SET IN-QUOTES TO TRUE
                       WHEN CH-COMMA AND NOT-IN-QUOTES
                           ADD 1 TO WS-N
                           MOVE ZERO TO WS-OUT-LEN
                       WHEN OTHER
                           PERFORM APPEND-CHAR
                   END-EVALUATE
               END-IF
           END-PERFORM.
       APPEND-CHAR.
           ADD 1 TO WS-OUT-LEN
           MOVE WS-CH TO WS-FIELD(WS-N)(WS-OUT-LEN:1).
line 01: 03 fields
  001 [9]
  002 [Ledgers, Old and New]
  003 [2]
line 02: 03 fields
  001 [10]
  002 [The "Batch" Handbook]
  003 [1]
line 03: 04 fields
  001 [11]
  002 [Plain Title]
  003 []
  004 [4]

The scanner keeps one state, whether it is inside quotes, and one output position. A comma outside quotes starts the next field, a quote toggles the state, and two quotes inside quotes append one quote character and skip the second. Appending uses a reference-modified, subscripted target WS-FIELD(WS-N)(WS-OUT-LEN:1). The empty third field of line 3 is simply a field of spaces. A quote that never closes would swallow the rest of the line, which a production loader should report. Condition names (CH-QUOTE, NEXT-QUOTE) keep the EVALUATE lines within the 72-column limit of fixed reference format; a longer line is silently cut at column 72 (see Reference Format: Fixed and Free), which turns the end of a condition into a compile error such as continuation character expected.

The scanner can be moved into a called subprogram (see Subprograms: CALL and Linkage) so that every loader program shares it.

Differences between the standard, Enterprise COBOL and GnuCOBOL

Topic ISO/IEC 1989:2023 Enterprise COBOL 6.5 GnuCOBOL 3.2

STRING, UNSTRING, INSPECT with scope terminators, POINTER, TALLYING, ON OVERFLOW

Yes

Yes, formats as on this page

Yes; behaviour in the tests above matches the IBM rules

INSPECT BACKWARD

Added in COBOL 2023 (as listed in the INCITS summary of changes)

Not in the INSPECT formats

Reserved word only; syntax error by default, parsed but "not implemented" under -std=mf

INSPECT ... TRAILING

Not verified (the standard is paywalled)

Not available (ALL, LEADING, FIRST only)

Accepted

STRING numeric sender

Not verified (the standard is paywalled)

Integer without P only

Accepted without a diagnostic, including non-integer and signed items

Reference modification

Yes

Yes; start and length must be positive and in range, checked at run time with SSRANGE

Yes; constants checked at compile time, variables only with -debug or -fec (a zero length was not rejected)

Dynamic-length items and LIMIT

COBOL 2014 dynamic-length items

6.3 and later: STRING receiver can be a dynamic-length item; a pointer beyond the length pads with spaces

Not available (see the pointers page)

UTF-8 (PIC U) operands

Not verified (the standard is paywalled)

6.3 and later: STRING, UNSTRING and reference modification accept USAGE UTF-8 operands; the INSPECT description lists only DISPLAY, DISPLAY-1 and NATIONAL

Experimental; see the national page

TRIM, UPPER-CASE, LOWER-CASE, REVERSE

Yes

Yes

Yes

CONCAT, SUBSTITUTE

Not verified (the standard is paywalled)

No

Yes

Where the book is outdated

Tom Taulli’s Modern Mainframe Development covers STRING, UNSTRING, INSPECT, reference modification, NUMVAL, NUMVAL-C and the case functions in its chapter 6. Those statements have not changed in a way that makes the basic forms wrong. What is out of date or missing is the surroundings. The book is written against Enterprise COBOL V6.3 as the newest release, so it cannot mention the 6.4 and 6.5 releases or the COBOL 2023 INSPECT BACKWARD phrase, which no shipping compiler implements yet (see above). Its examples were written for GnuCOBOL through OpenCobolIDE, which is no longer maintained; use GnuCOBOL 3.2 with the diagnostics shown on this page. The remarks on this page about padding, truncation, unset pointers and numeric senders are the situations where a short example works and a real file does not, so test with the lengths and line ends of your real data.