National UTF-8 and Code Pages

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; an edition 4 is upcoming, in DIS ballot), GnuCOBOL 3.2 (28 July 2023, the latest release) and IBM Enterprise COBOL for z/OS 6.5 (PTFs through September 2026), and was checked on 2026-10-20. Every GnuCOBOL example below was compiled with cobc -x -Wall (GnuCOBOL 3.2.0, fixed reference format) in a container and run, and the output and the diagnostics shown are real. Where a feature is unfinished in GnuCOBOL the example says so and shows the warning. The Enterprise COBOL example is labelled "z/OS; not run here": it was checked statement by statement against the IBM manuals and was not compiled. The ISO standard is paywalled and was not read; the rules for the standard come from the vendor manuals, which say where they follow it. National data is a COBOL 2002 feature; USAGE UTF-8 and PIC U are an IBM extension introduced in Enterprise COBOL 6.3.

A COBOL program stores text as bytes, and the same byte means different letters in different code pages. For fifty years that was a problem only when a file moved between a mainframe (EBCDIC) and everything else (ASCII). Today the problem is also names with accents, branch libraries in other languages and JSON documents that are always UTF-8. This page explains the three families of character data a COBOL program can hold (single-byte code-page text, national UTF-16 and UTF-8), the conversions between them, the collating sequence that decides which of two strings is smaller, and how much of this GnuCOBOL 3.2 supports. The data descriptions themselves are in The PICTURE Clause and USAGE and Numeric Representation; the literal forms are in Words, Literals and Figurative Constants.

Characters, code pages and encodings

A character set is a list of characters; a coded character set (a code page) assigns each one a numeric code; the code page has a CCSID, a number from 1 to 65536 (Enterprise COBOL Programming Guide, Unicode and the encoding of language characters). A Unicode character has a code point, and an encoding (UTF-8, UTF-16) says how code points become bytes.

Name Encoding Where COBOL meets it

EBCDIC (for example CCSID 1140, the euro variant of 37)

one byte per character, the mainframe code page

The default for PIC X data on z/OS: literals, DISPLAY fields, files. CODEPAGE(1140) is the Enterprise COBOL default.

ASCII (for example 20127 7-bit, 819 ISO 8859-1, 1252 Windows)

one byte per character

The native code page of GnuCOBOL on Linux, macOS and Windows; z/OS UNIX files and data exchanged with distributed systems.

DBCS and mixed SBCS/DBCS (for example 930, 939)

two bytes per character, shift-out and shift-in delimiters inside a mixed string

USAGE DISPLAY-1 items, DBCS literals and Japanese, Korean and Chinese data. Not covered further here.

UTF-16 big-endian (CCSID 1200)

two bytes per code unit; one unit for almost every character, two (a surrogate pair) for the rest

USAGE NATIONAL, PIC N, N"..." literals.

UTF-8 (CCSID 1208)

one to four bytes per character; the ASCII characters are unchanged

USAGE UTF-8, PIC U, U"..." literals (Enterprise COBOL 6.3 and later), and any UTF-8 text held in a plain PIC X item on GnuCOBOL.

The ASCII characters have the same code in UTF-8 as in ASCII, and the letter A is the single byte X"41" there. In UTF-16 the same letter is the two bytes NX"0041", and ABC is NX"004100420043". Enterprise COBOL keeps national data in big-endian order: UTF-16LE data from another platform must be converted before the program treats it as national.

Collating sequence: why a sort order changes when code leaves the mainframe

A collating sequence is the order of the characters of a code page. It decides whether "a" is less than "A", where the digits fall relative to the letters, and which character is HIGH-VALUE. The EBCDIC and ASCII orders disagree in a way that affects programs every day:

Order, lowest first EBCDIC (for example CCSID 1140) ASCII

Letters and digits

lowercase letters, then uppercase letters, then digits

digits, then uppercase letters, then lowercase letters

"a" against "A"

"a" is less

"a" is greater

"1" against "A"

"1" is greater

"1" is less

HIGH-VALUE

X"FF"

X"FF" (the highest byte of the native sequence)

Gaps

the 26 letters of each case are not contiguous: there are unused codes between I and J, R and S, i and j, r and s

letters are contiguous

So a master file sorted on the mainframe by a key that mixes digits and letters is in a different order from the same file sorted on a PC, and a program that compares keys, or merges two files, gives different results on the two platforms. The data is the same; the sequence differs. The example sorts five shelf labels of the Circulation library twice: first in the native order of the compiler (ASCII here), then with the COLLATING SEQUENCE phrase naming an alphabet defined as EBCDIC in SPECIAL-NAMES. The table form of SORT is covered in Searching and Sorting Tables; the file form in SORT and MERGE.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. SHELFSORT.
      * Shelf labels sorted twice: native order, then EBCDIC order.
       ENVIRONMENT DIVISION.
       CONFIGURATION SECTION.
       SPECIAL-NAMES.
           ALPHABET EBC IS EBCDIC.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  SHELF-TABLE.
           05  SHELF-LABEL     PIC X(4) OCCURS 5 TIMES
                               INDEXED BY S-IX.
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE "b-02" TO SHELF-LABEL (1)
           MOVE "A-01" TO SHELF-LABEL (2)
           MOVE "7-03" TO SHELF-LABEL (3)
           MOVE "a-04" TO SHELF-LABEL (4)
           MOVE "B-05" TO SHELF-LABEL (5)
           SORT SHELF-LABEL ON ASCENDING KEY SHELF-LABEL
           DISPLAY "NATIVE (ASCII) ORDER:"
           PERFORM SHOW-TABLE
           SORT SHELF-LABEL ON ASCENDING KEY SHELF-LABEL
               COLLATING SEQUENCE IS EBC
           DISPLAY "EBCDIC ORDER:"
           PERFORM SHOW-TABLE
           STOP RUN.
       SHOW-TABLE.
           PERFORM VARYING S-IX FROM 1 BY 1 UNTIL S-IX > 5
               DISPLAY "  " SHELF-LABEL (S-IX)
           END-PERFORM.

The output is real. The digit comes first in ASCII and last in EBCDIC, and the lowercase labels come before the uppercase ones in EBCDIC only:

NATIVE (ASCII) ORDER:
  7-03
  A-01
  B-05
  a-04
  b-02
EBCDIC ORDER:
  a-04
  b-02
  A-01
  B-05
  7-03

PROGRAM COLLATING SEQUENCE and ALPHABET

Two clauses set the sequence for the alphanumeric items of a whole program. The ALPHABET clause of SPECIAL-NAMES names an alphabet; the PROGRAM COLLATING SEQUENCE clause of the OBJECT-COMPUTER paragraph says which alphabet the program uses. The alphabet-name can also be given to the COLLATING SEQUENCE phrase of one SORT or MERGE (as above), and to CODE-SET (below) or SYMBOLIC CHARACTERS.

An alphabet is one of:

  • STANDARD-1: the ASCII character set (ANSI INCITS X3.4);

  • STANDARD-2: the ISO/IEC 646 international reference version, a 7-bit set;

  • NATIVE and EBCDIC: in Enterprise COBOL both mean the EBCDIC sequence, the default when no alphabet is given; in GnuCOBOL NATIVE is the system’s own set and ASCII, STANDARD-1 and STANDARD-2 are treated as interchangeable;

  • a literal list: "A" ALSO "a" puts both characters at the same position, "Z" THROUGH "A" reverses a range, and a list of numbers or characters sets the order in which the characters appear. Characters that are not listed come after the listed ones, and a character may not appear twice in one alphabet.

Enterprise COBOL applies the program’s sequence to comparisons in relation conditions and condition-name conditions, to the values of HIGH-VALUE and LOW-VALUE, to SEARCH ALL, and to SORT and MERGE unless the statement has its own COLLATING SEQUENCE phrase. It applies only to alphabetic and alphanumeric keys: the clause does not affect comparisons that involve national or DBCS operands. When the alphabet defines a position shared by several characters through ALSO, HIGH-VALUE is the last character listed at the highest position and LOW-VALUE the first character listed at the lowest. Changing the EBCDIC sequence in SPECIAL-NAMES changes the whole sequence, not only the characters named.

The next program matches a member name regardless of case, which is the textbook use of ALSO: a front-desk clerk types ada lovelace, and the master record holds ADA LOVELACE. The comparison is between data items of the Circulation record layout (see Copybooks and REPLACE).

       IDENTIFICATION DIVISION.
       PROGRAM-ID. NOCASE.
      * Case-insensitive member-name match with a user alphabet.
       ENVIRONMENT DIVISION.
       CONFIGURATION SECTION.
       OBJECT-COMPUTER. CIRC-HOST
           PROGRAM COLLATING SEQUENCE IS NO-CASE.
       SPECIAL-NAMES.
           ALPHABET NO-CASE IS
               "A" ALSO "a"  "B" ALSO "b"  "C" ALSO "c"
               "D" ALSO "d"  "E" ALSO "e"  "F" ALSO "f"
               "G" ALSO "g"  "H" ALSO "h"  "I" ALSO "i"
               "J" ALSO "j"  "K" ALSO "k"  "L" ALSO "l"
               "M" ALSO "m"  "N" ALSO "n"  "O" ALSO "o"
               "P" ALSO "p"  "Q" ALSO "q"  "R" ALSO "r"
               "S" ALSO "s"  "T" ALSO "t"  "U" ALSO "u"
               "V" ALSO "v"  "W" ALSO "w"  "X" ALSO "x"
               "Y" ALSO "y"  "Z" ALSO "z".
       DATA DIVISION.
       WORKING-STORAGE SECTION.
           COPY MEMBREC.
       01  WS-SEARCH-NAME      PIC X(30) VALUE "ada lovelace".
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE 100 TO MEMBER-ID
           MOVE "ADA LOVELACE" TO MEMBER-NAME
           IF MEMBER-NAME = WS-SEARCH-NAME
               DISPLAY "SAME MEMBER: " MEMBER-ID
           ELSE
               DISPLAY "DIFFERENT NAME"
           END-IF
           STOP RUN.
SAME MEMBER: 000100

The same compile also passes with -std=ibm. The IBM manual shows the same technique, with a complete alphabet, as the way to make sorting and comparing treat upper and lower case alike.

Switching the whole program without editing it

GnuCOBOL can change its default sequence from the command line. cobc --help lists -fdefault-colseq=[ASCII|EBCDIC|NATIVE] ("define default collating sequence"); the option is not described in the HTML manual of 3.2. The program has no ALPHABET at all:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. COLSEQ.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  W-LOWER             PIC X VALUE "a".
       01  W-UPPER             PIC X VALUE "A".
       01  W-DIGIT             PIC X VALUE "1".
       PROCEDURE DIVISION.
       MAIN-PARA.
           IF W-LOWER < W-UPPER
               DISPLAY "a sorts before A"
           ELSE
               DISPLAY "a sorts after A"
           END-IF
           IF W-DIGIT < W-UPPER
               DISPLAY "1 sorts before A"
           ELSE
               DISPLAY "1 sorts after A"
           END-IF
           STOP RUN.

Compiled as cobc -x -Wall colseqflag.cob the output is the ASCII one; compiled with -fdefault-colseq=EBCDIC it is the mainframe one:

a sorts after A
1 sorts before A
a sorts before A
1 sorts after A

Source: GnuCOBOL manual: Compiler options (the -fdefault-colseq option was found with cobc --help)

GnuCOBOL 3.2 pitfalls found while testing

Two behaviours of GnuCOBOL 3.2.0 differ from what its manual and the IBM manual describe, and are worth knowing before trusting a custom alphabet.

  • FUNCTION ORD is documented as the ordinal position of a character in the program’s collating sequence. In 3.2.0 it ignores the alphabet: with PROGRAM COLLATING SEQUENCE set to EBCDIC or to the case-folding alphabet above, FUNCTION ORD("A") still returns 66, the native position plus one, while relation conditions and SORT do honour the alphabet. Use ORD only for the native order.

  • A relation condition between two literals is evaluated by the compiler, not at run time, and the evaluation ignores ALSO. The program below tests the same pair of characters twice, once through data items and once through literals:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. NOFOLD.
      * Literal-to-literal test under a user alphabet (3.2 quirk).
       ENVIRONMENT DIVISION.
       CONFIGURATION SECTION.
       OBJECT-COMPUTER. CIRC-HOST
           PROGRAM COLLATING SEQUENCE IS NO-CASE.
       SPECIAL-NAMES.
           ALPHABET NO-CASE IS "A" ALSO "a".
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  W-LOWER             PIC X VALUE "a".
       01  W-UPPER             PIC X VALUE "A".
       PROCEDURE DIVISION.
       MAIN-PARA.
           IF W-LOWER = W-UPPER
               DISPLAY "ITEMS: a = A"
           END-IF
           IF "a" = "A"
               DISPLAY "LITERALS: a = A"
           ELSE
               DISPLAY "LITERALS: a NOT = A"
           END-IF
           STOP RUN.

This example deliberately warns. The compiler says so, and the two answers disagree:

nocasefold.cob: in paragraph 'MAIN-PARA':
nocasefold.cob:19: warning: expression 'a' EQUALS 'A' is always FALSE [-Wconstant-expression]
ITEMS: a = A
LITERALS: a NOT = A

Test with data items, as the first example does. A literal compared with a literal is rarely useful anyway.

Files and code pages: CODE-SET and translation tables

When a file crosses the boundary the data must be translated, not only the order. On z/OS the translation is usually done by the transfer tool (for example a text-mode file transfer that converts between EBCDIC and ASCII). Enterprise COBOL 6.5 can also convert tagged z/OS UNIX source files automatically when the file’s code page does not match the CODEPAGE option of the compile job; that covers program text, not the data files a program reads. Inside a program the standard tool is the CODE-SET clause of the FD: it names an alphabet, and the runtime translates the record data between the program’s native character set and that alphabet as it reads or writes. GnuCOBOL accepts it for sequential and line sequential files.

The next program writes one Circulation-style record through a file described with CODE-SET IS EBC, then reads the same file back through a second description with no CODE-SET and shows the raw bytes:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. EBCFILE.
      * Write a record in EBCDIC with CODE-SET, read it back raw.
       ENVIRONMENT DIVISION.
       CONFIGURATION SECTION.
       SPECIAL-NAMES.
           ALPHABET EBC IS EBCDIC.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT EXPORT-FILE ASSIGN TO "ebcdic.dat"
               ORGANIZATION IS LINE SEQUENTIAL.
           SELECT RAW-FILE ASSIGN TO "ebcdic.dat"
               ORGANIZATION IS LINE SEQUENTIAL.
       DATA DIVISION.
       FILE SECTION.
       FD  EXPORT-FILE CODE-SET IS EBC.
       01  EXPORT-REC          PIC X(8).
       FD  RAW-FILE.
       01  RAW-REC             PIC X(8).
       PROCEDURE DIVISION.
       MAIN-PARA.
           OPEN OUTPUT EXPORT-FILE
           MOVE "ABC 123" TO EXPORT-REC
           MOVE X"E9" TO EXPORT-REC (8:1)
           WRITE EXPORT-REC
           CLOSE EXPORT-FILE
           OPEN INPUT RAW-FILE
           READ RAW-FILE
           DISPLAY "RAW BYTES: " FUNCTION HEX-OF(RAW-REC)
           CLOSE RAW-FILE
           STOP RUN.

The compiler labels CODE-SET unfinished in 3.2 (this example warns, and the warning is real), but the translation happens: A, B, C, the space and 1, 2, 3 become the EBCDIC bytes C1 C2 C3 40 F1 F2 F3.

ebcdicfile.cob:16: warning: handling of CODE-SET is unfinished; implementation is likely to be changed [-Wunfinished]
RAW BYTES: C1C2C340F1F2F33A

The last byte is the Latin-1 letter X"E9" (é), a character outside the 7-bit set where the tables disagree. The default translation table of GnuCOBOL maps it to 3A; running the same source with -febcdic-table=ebcdic500_latin1, one of the tables in the config directory of the installation, gives 51, the EBCDIC 500 code of that letter:

RAW BYTES: C1C2C340F1F2F351

The manual of 3.2 lists the values DEFAULT, RESTRICTED-GC, IBM and GCOS for -febcdic-table; the table name above was accepted and changed the output. A table that does not match the actual code page of the partner system corrupts the accented letters and leaves the plain ones intact, so the corruption goes unnoticed in tests that use only plain ASCII names. The related option -fsign=EBCDIC for the sign nibble of DISPLAY numbers is in USAGE and Numeric Representation.

National data: USAGE NATIONAL and PIC N (UTF-16)

National data stores each character as a UTF-16 code unit in big-endian order (CCSID 1200). It was added by COBOL 2002; in Enterprise COBOL it is the older of the two Unicode types. Its pieces are:

  • USAGE NATIONAL with a PICTURE of N symbols defines a national item, one N per character (two bytes for almost every character, four bytes for a character outside the Basic Multilingual Plane, which uses two code units). With a PICTURE of N symbols and at least one B, 0 or / it is national-edited. With a numeric-edited PICTURE such as -$999.99 and USAGE NATIONAL it is a national numeric-edited item. National decimal and national floating-point items exist too.

  • The NSYMBOL compiler option decides what a bare PIC N and the N"..." prefix mean. NSYMBOL(NATIONAL) (the default in 6.5) makes N national, as the 2002 standard says; NSYMBOL(DBCS) makes it a DBCS item and a DBCS literal, for programs written before Unicode support.

  • National literals are N"text" or N'text', and NX"hhhh..." is the hexadecimal form. ALL national-literal and the figurative constants SPACE, ZERO, QUOTE, HIGH-VALUE and LOW-VALUE take national values in national contexts.

  • GROUP-USAGE NATIONAL makes a group that contains only national items behave like one elementary national item.

Alphabetic and alphanumeric items, DBCS items and integers are converted to national implicitly by MOVE, and national is converted to numeric by MOVE. Comparisons convert in the same way. The compiler does this using the CODEPAGE value; the program has to take care when the data is in another code page (see below). Assigning X"C1C2" to a national item moves the characters that the bytes represent in the CODEPAGE, not the bytes C1C2, which is why the manual recommends not using alphanumeric hexadecimal literals where a national literal is meant.

Converting explicitly: NATIONAL-OF and DISPLAY-OF

Two intrinsic functions convert on purpose, with an optional second argument that names the other code page by its CCSID:

  • FUNCTION NATIONAL-OF(argument-1 [argument-2]) returns a national string. The first argument is alphabetic, alphanumeric, UTF-8 or DBCS; the second names the source code page. Without it the source is the CODEPAGE value, or 1208 when the first argument is UTF-8.

  • FUNCTION DISPLAY-OF(argument-1 [argument-2]) returns an alphanumeric string. The first argument is national or UTF-8; the second names the output code page (EBCDIC, ASCII, EUC or UTF-8). Without it the output is the CODEPAGE value. Specify 1208 to get UTF-8 bytes in a PIC X item.

A character that has no counterpart in the target code page does not raise an exception: it becomes a substitution character. NATIONAL-OF produces X"001A" for a single-byte source character and X"FFFD" for a multi-byte one. DISPLAY-OF produces X"3F" for single-byte EBCDIC, X"7F" for single-byte ASCII and Windows code pages, and for a UTF-8 output X"1A" or X"EFBFBD". What can fail is a missing code-page table: if z/OS Unicode conversion services are not installed with the table for the conversion, the call ends with a severe runtime error (a severity-3 Language Environment condition).

The common mistake is a conversion with the wrong code page, which succeeds silently. The Programming Guide’s own example is a file in Greek (CCSID 875): a plain MOVE converts it as if it were in the program’s code page, so the plain letters survive and the Greek letters turn into other characters; NATIONAL-OF(field 875) converts it correctly. If you override the code page in DISPLAY-OF, do not use the result in an operation with an implicit conversion (an assignment to a national item, a comparison with one): that conversion assumes the CODEPAGE code page and would convert the data a second time.

UTF-8 data: USAGE UTF-8 and PIC U (Enterprise COBOL 6.3 and later)

UTF-16 is convenient because one position is one character, but the world exchanges UTF-8 (JSON, HTML, Db2 Unicode tables, MQ messages). Enterprise COBOL 6.3 added native UTF-8 items, and the 6.5 What’s New page lists further UTF-8 enhancements: moves from a UTF-8 sender to numeric items, comparisons with numeric operands, and ULENGTH accepting UTF-8 arguments. There are three kinds of UTF-8 item, all declared with the U picture symbol (with USAGE UTF-8 implied or explicit):

Kind Declaration Storage and behaviour

Fixed character-length

01 U-NAME PIC U(30).

Holds 30 characters and reserves 4 bytes for each (120 bytes) because a character can take up to four. Padded with UTF-8 blanks X"20"; truncation is at a character boundary.

Fixed byte-length

01 U-KEY PIC U BYTE-LENGTH 30.

Exactly 30 bytes; the number of characters varies. For compatibility with Db2, DFSORT and MQ, which have fixed byte-length UTF-8 strings. Use it as a Db2 host variable and as a sort key.

Dynamic length

01 U-TEXT PIC U DYNAMIC LENGTH LIMIT 200.

No fixed character count; the byte length is that of the current content. Not allowed in a file record. See Pointers and Dynamic Storage.

The rules around UTF-8 items:

  • A UTF-8 item may be elementary or sit in a group, including a file record (except a dynamic-length one); a group declared with GROUP-USAGE UTF-8 can contain only fixed byte-length UTF-8 items. A condition variable of an 88-level can be UTF-8.

  • There is no UTF-8 edited, decimal or floating-point item: USAGE UTF-8 requires the U symbol, so a numeric item cannot be UTF-8.

  • The VALUE clause takes alphanumeric, national or UTF-8 literals; an alphanumeric literal is converted from the CODEPAGE to UTF-8.

  • UTF-8 literals are U"text" or U'text'. The text is converted from EBCDIC, and may contain Unicode escapes: \u followed by four hexadecimal digits for a code point of the Basic Multilingual Plane, and \U00 followed by six hexadecimal digits for any code point. U'caf\u00E9' is the word café. A backslash is written \\ when it must be literal. A basic literal may produce at most 160 bytes after conversion. The hexadecimal form is UX"hh...", from 2 to 320 digits, and the bytes are checked to be valid UTF-8.

  • Reference modification of a UTF-8 item counts characters, not bytes, so U-NAME (3:2) is the third and fourth code points wherever their bytes lie. The compiler cannot know the byte offset in advance, so such expressions cost a scan at run time.

  • Comparisons are byte by byte in binary, which gives the same order as comparing the national forms of the same text when all the code points are in the Basic Multilingual Plane. A UTF-8 item can be compared with alphabetic, alphanumeric, national and UTF-8 operands. HIGH-VALUE for UTF-8 is UX'F48FBFBF' (the code point 10FFFF) and does not compare equal to HIGH-VALUE of an alphanumeric or national item, because the value is not translated between the data types.

  • MOVE converts among alphabetic, alphanumeric, national and UTF-8; padding and truncation always occur at a character boundary. MOVE from UTF-8 to alphanumeric is not allowed: use DISPLAY-OF.

  • Statements that support UTF-8 include IF, EVALUATE, INITIALIZE (the default is a UTF-8 space), MOVE, STRING, UNSTRING, SORT, MERGE, JSON GENERATE, JSON PARSE, ALLOCATE and FREE. ACCEPT, INSPECT, XML GENERATE and XML PARSE do not; move the item to a national item and use that. The Db2 precompiler cannot be used with UTF-8 items, and a UTF-8 host variable is always taken as CCSID 1208 by the Db2 coprocessor.

The Unicode intrinsic functions

When the data is kept as UTF-8 in either a UTF-8 item or a PIC X item that holds UTF-8, these functions work in characters:

Function Result

ULENGTH(arg [start length])

the number of characters (code points) in the argument, which may be alphanumeric, UTF-8 or national; optionally of a byte range. Combining characters are counted individually, so ä written as one code point counts 1 and as a plus a combining diaeresis counts 2.

USUBSTR(arg start length)

the substring starting at character start with length characters. For PIC X and UTF-8 arguments the result is UTF-8 bytes; for national, UTF-16 units.

UPOS(arg n)

the byte position at which the n-th character starts (zero if n is out of range).

UWIDTH(arg n)

the width in bytes of the n-th character.

UVALID(arg)

zero if the data is valid UTF-8 (or UTF-16), otherwise the position of the first invalid element.

USUPPLEMENTARY(arg)

tells whether a valid string contains a supplementary code point (above 65535), which needs two UTF-16 units; check it before converting to national if one position must be one character.

A conversion example (z/OS; not run here)

The sketch below converts the member name of the Circulation MEMBREC layout, held as EBCDIC text, to the two Unicode forms, and takes the character count and the first character. It was checked against the Enterprise COBOL 6.5 manuals above and was not compiled (GnuCOBOL 3.2 cannot compile it, as the next section shows). The CBL statement sets the code page to the default 1140.

       CBL CODEPAGE(1140),NSYMBOL(NATIONAL)
       IDENTIFICATION DIVISION.
       PROGRAM-ID. MEMBCONV.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
           COPY MEMBREC.
       01  WS-NAME-NAT         PIC N(30) USAGE NATIONAL.
       01  WS-NAME-U8          PIC U(30) USAGE UTF-8.
       01  WS-NAME-WIRE        PIC X(120).
       01  WS-INITIAL          PIC U(1)  USAGE UTF-8.
       01  WS-CHARS            PIC 9(4)  BINARY.
       PROCEDURE DIVISION.
       MAIN-PARA.
      * EBCDIC (CCSID 1140) to UTF-16, then UTF-16 to UTF-8
           MOVE FUNCTION NATIONAL-OF(MEMBER-NAME) TO WS-NAME-NAT
           MOVE WS-NAME-NAT TO WS-NAME-U8
      * the same result in one implicit conversion
           MOVE MEMBER-NAME TO WS-NAME-U8
      * UTF-8 bytes in a PIC X item, for a wire format
           MOVE FUNCTION DISPLAY-OF(WS-NAME-NAT 1208)
               TO WS-NAME-WIRE
      * characters, not bytes
           COMPUTE WS-CHARS = FUNCTION ULENGTH
               (FUNCTION TRIM(WS-NAME-U8 TRAILING))
           MOVE FUNCTION USUBSTR(WS-NAME-U8 1 1) TO WS-INITIAL
           STOP RUN.

For a Db2 column of type CHAR(30) in a Unicode table, the host variable would be 01 HV-NAME PIC U BYTE-LENGTH 30., whose byte length must match the column to avoid unexpected truncation or padding.

flowchart LR A["Alphanumeric PIC X: EBCDIC or ASCII, CODEPAGE option"] N["National PIC N: UTF-16BE, CCSID 1200"] U["UTF-8 PIC U: CCSID 1208"] A -- "MOVE, NATIONAL-OF" --> N N -- "DISPLAY-OF" --> A A -- "MOVE" --> U U -- "DISPLAY-OF with code page" --> A N -- "MOVE" --> U U -- "MOVE, NATIONAL-OF" --> N

The figure shows the conversion routes of Enterprise COBOL 6.5. A MOVE from alphanumeric converts using the CODEPAGE; to get alphanumeric back from national or UTF-8, DISPLAY-OF is required, and its code page argument chooses whether the result is EBCDIC, ASCII or UTF-8 bytes.

The CODEPAGE compiler option

CODEPAGE(ccsid) (abbreviation CP) names the EBCDIC code page for compile-time and run-time operations that depend on the encoding. The default is CODEPAGE(1140), the same as CCSID 37 plus the euro sign. It decides:

  • the encoding of alphanumeric, national, UTF-8 and DBCS literals in the source, and the default encoding of alphanumeric and DBCS data at run time;

  • conversions of alphanumeric data to or from national and UTF-8 in moves, comparisons, NATIONAL-OF and DISPLAY-OF when the call gives no code page;

  • DBCS names and encodings of XML documents, INVOKE and class and method definitions, SQL host variables when SQLCCSID is in effect, and the source text of EXEC SQL.

It does not change national items (always UTF-16 big-endian, CCSID 1200), UTF-8 items (always 1208), or the characters of the basic COBOL set: the compiler always reads the dollar sign as X"5B" and the quotation mark as X"7F", as in CCSID 1140. Programs that use DBCS names, DISPLAY-1 items or DBCS literals must compile with one of the mixed EBCDIC multibyte CCSIDs (for example 930, 939, 933, 935 or 937), and a program compiled with the TEST option must use the CCSID of the source. NATIONAL-OF, DISPLAY-OF, XML processing and a few other operations can override the option with an explicit CCSID, but an option set wrongly for the whole program is the more frequent cause of garbled accents on a mainframe: the source was saved in one code page and compiled with another, so every literal is translated wrongly while the data is fine. Compiler options are collected in Performance and Compiler Options.

What GnuCOBOL 3.2 supports

GnuCOBOL has no code-page option: the source, the data and the screen use the bytes of the system, which is UTF-8 on a modern Linux or macOS terminal. Its support for national and UTF-8 data is partial and, in 3.2, labelled unfinished by the compiler itself. The Programmer’s Guide says that national support is "currently only partially implemented" and that USAGE NATIONAL is "syntactically recognized" but not supported, and cobc --list-intrinsics marks NATIONAL-OF, DISPLAY-OF and CHAR-NATIONAL as not implemented. This table shows what GnuCOBOL 3.2.0 does in practice, found by compiling and running the programs on this page:

Feature Enterprise COBOL 6.5 GnuCOBOL 3.2.0

PIC N(4) (national)

8 bytes of UTF-16BE, padded with 0020

accepted with a warning that national handling is unfinished; 8 bytes, but padded with the bytes 2020 2020; LENGTH is 4 and BYTE-LENGTH is 8

N"AB", NX"..." literals

national literals

accepted with the same kind of warning (national-literals and hexadecimal-national-literals are ok in default.conf; the non-standard NC"..." form is set to warning)

PIC U(4)

16 bytes of UTF-8, padded with X"20"

accepted with no warning; 16 bytes, padded with X"20", holds whatever bytes are moved in

USAGE UTF-8 written explicitly

UTF-8 item

accepted with a warning that handling of USAGE UTF-8 is unfinished

NATIONAL-OF, DISPLAY-OF

conversion functions

compile error: "is not implemented"

ULENGTH, USUBSTR, UPOS, UWIDTH, UVALID, USUPPLEMENTARY

Unicode functions

not in the function list of cobc ("unknown" for ULENGTH and USUBSTR)

MOVE national to alphanumeric

converts using the code page

compile error: "invalid MOVE statement"

CODE-SET on a file

translation of file data

works, with an "unfinished" warning

Collating sequence, ALPHABET, SORT ... COLLATING SEQUENCE

supported

supported for alphanumeric data; see the pitfalls above

The next program shows the two storage items on a real compile. The warnings are part of the evidence; the program is a probe, not a recommendation:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. NATSTAT.
      * What GnuCOBOL 3.2 does with PIC N and PIC U items.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-NAT              PIC N(4).
       01  WS-UTF              PIC U(4).
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE N"AB" TO WS-NAT
           DISPLAY "N BYTES " FUNCTION BYTE-LENGTH(WS-NAT)
           DISPLAY "N HEX   " FUNCTION HEX-OF(WS-NAT)
           MOVE "José" TO WS-UTF
           DISPLAY "U BYTES " FUNCTION BYTE-LENGTH(WS-UTF)
           DISPLAY "U HEX   " FUNCTION HEX-OF(WS-UTF)
           STOP RUN.
natstatus.cob:6: warning: handling of USAGE NATIONAL is unfinished; implementation is likely to be changed [-Wunfinished]
natstatus.cob: in paragraph 'MAIN-PARA':
natstatus.cob:10: warning: handling of national literal is unfinished; implementation is likely to be changed [-Wunfinished]
N BYTES 8
N HEX   0041004220202020
U BYTES 16
U HEX   4A6F73C3A92020202020202020202020

The national item holds 0041 0042 correctly but its blanks are two-byte 2020 pairs, not the UTF-16 space 0020, which is a difference from the IBM storage layout that a program exchanging national data would notice. The PIC U(4) item holds José in the five bytes 4A 6F 73 C3 A9 and then blanks: it takes the bytes of the source as they come, and nothing validates them.

Code that calls the IBM Unicode functions does not compile. This example is deliberately wrong for GnuCOBOL 3.2:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. NATFAIL.
      * Deliberately wrong for GnuCOBOL 3.2: IBM Unicode functions.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-NAME             PIC X(20) VALUE "José Müller".
       01  WS-NAT              PIC N(20).
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE FUNCTION NATIONAL-OF(WS-NAME) TO WS-NAT
           MOVE WS-NAT TO WS-NAME
           DISPLAY FUNCTION ULENGTH(WS-NAME)
           DISPLAY FUNCTION USUBSTR(WS-NAME 1 4)
           STOP RUN.

The real messages (the cobc output also prints the source line after each, left out here):

natfail.cob:7: warning: handling of USAGE NATIONAL is unfinished; implementation is likely to be changed [-Wunfinished]
natfail.cob:10: error: FUNCTION 'NATIONAL-OF' is not implemented
natfail.cob:11: error: invalid MOVE statement
natfail.cob:12: error: FUNCTION 'ULENGTH' unknown
natfail.cob:13: error: FUNCTION 'USUBSTR' unknown

UTF-8 text in ordinary PIC X items

The portable way to handle accents on GnuCOBOL is the one programmers used before Unicode support: keep the text in PIC X, accept that it is UTF-8 when the terminal and the files are, and count bytes carefully. A byte of the UTF-8 encoding is a continuation byte when its value is from 128 to 191 (X"80" to X"BF"); every other byte starts a character. So the number of characters is the number of non-continuation bytes, and a cut must not fall in front of a continuation byte. The next program uses the MEMBER-NAME field of the Circulation layout, shows that LENGTH counts bytes, counts characters, demonstrates what a plain reference modification does to the text, and cuts it safely:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. UTF8BYTE.
      * UTF-8 text in a PIC X item: bytes are not characters.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
           COPY MEMBREC.
       01  WS-I                PIC 9(3).
       01  WS-BYTES            PIC 9(3).
       01  WS-CHARS            PIC 9(3).
       01  WS-VAL              PIC 9(3).
       01  WS-LIMIT            PIC 9(3) VALUE 4.
       01  WS-SHORT            PIC X(4).
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE 100 TO MEMBER-ID
           MOVE "José Müller" TO MEMBER-NAME
           COMPUTE WS-BYTES = FUNCTION LENGTH
               (FUNCTION TRIM(MEMBER-NAME TRAILING))
           MOVE ZERO TO WS-CHARS
           PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > WS-BYTES
               COMPUTE WS-VAL = FUNCTION ORD(MEMBER-NAME (WS-I:1)) - 1
               IF WS-VAL < 128 OR WS-VAL > 191
                   ADD 1 TO WS-CHARS
               END-IF
           END-PERFORM
           DISPLAY "BYTES: " WS-BYTES " CHARACTERS: " WS-CHARS
           MOVE MEMBER-NAME (1:WS-LIMIT) TO WS-SHORT
           DISPLAY "FIRST 4 BYTES: " FUNCTION HEX-OF(WS-SHORT)
           PERFORM UNTIL WS-LIMIT = 0
               COMPUTE WS-VAL =
                   FUNCTION ORD(MEMBER-NAME (WS-LIMIT + 1:1)) - 1
               IF WS-VAL < 128 OR WS-VAL > 191
                   EXIT PERFORM
               END-IF
               SUBTRACT 1 FROM WS-LIMIT
           END-PERFORM
           MOVE SPACES TO WS-SHORT
           MOVE MEMBER-NAME (1:WS-LIMIT) TO WS-SHORT
           DISPLAY "SAFE CUT AT " WS-LIMIT " BYTES: [" WS-SHORT "]"
           DISPLAY "UPPER-CASE: "
               FUNCTION UPPER-CASE(MEMBER-NAME (1:WS-BYTES))
           STOP RUN.
BYTES: 013 CHARACTERS: 011
FIRST 4 BYTES: 4A6F73C3
SAFE CUT AT 003 BYTES: [Jos ]
UPPER-CASE: JOSé MüLLER

The name José Müller has 11 characters but 13 bytes. Cutting at four bytes keeps 4A 6F 73 C3: the C3 is the first half of é (C3 A9), and a file or a report that receives it holds an invalid UTF-8 sequence. The safe cut backs off to three bytes. The last line shows the third pitfall: UPPER-CASE and LOWER-CASE work byte by byte, so the accented letters keep their case. The same limits apply to the fixed-width layout itself: a PIC X(30) field holds 30 bytes, not 30 characters, so a name of 30 accented letters does not fit, and a report column built with PIC X loses alignment by one position for each accented letter. FUNCTION LOCALE-COMPARE is listed by cobc --list-intrinsics as implemented for comparisons that follow the locale; it was not exercised here.

Choosing a representation

Situation On Enterprise COBOL for z/OS On GnuCOBOL 3.2

Records and files shared with other mainframe programs

PIC X in the EBCDIC CODEPAGE; keep the CODEPAGE option the same in every program that shares the layout

PIC X; CODE-SET or an external translation step when the partner is EBCDIC

Text in many languages, processed character by character

USAGE NATIONAL (one position, one character for nearly all text)

keep UTF-8 in PIC X and count bytes yourself, as above

Data exchanged as UTF-8 (JSON, HTML, Db2 Unicode, MQ)

USAGE UTF-8; PIC U BYTE-LENGTH for Db2 host variables and sort keys; PIC U DYNAMIC LENGTH for variable data

PIC X

A statement that does not support UTF-8 (INSPECT, ACCEPT)

move to a national item, use the statement, convert back

INSPECT works on bytes

A different sort order than the machine’s

PROGRAM COLLATING SEQUENCE or the COLLATING SEQUENCE phrase

-fdefault-colseq, an ALPHABET, or the phrase

JSON generation and parsing in UTF-8 are in JSON GENERATE and PARSE; the Db2 coprocessor is in Embedded SQL and Db2; the file organizations that depend on key order are in Indexed Files and VSAM.

Where the book is outdated

The book consulted for this section (Taulli, Modern Mainframe Development, 2022, written for Enterprise COBOL 6.3) presents text as PIC X data and does not cover national or UTF-8 data; nothing is copied from it. Three points follow from that:

  • Treating every character as one byte is the 1985 model. Since COBOL 2002 the standard has national data, and since Enterprise COBOL 6.3 IBM has USAGE UTF-8, which the 6.5 release extends; a book that predates or omits these will not prepare you for a name with an accent.

  • The collating sequence is not a property of the language but of the code page: a program that behaves on a mainframe because its keys are in EBCDIC order may change its results when it is recompiled with GnuCOBOL or moved to a PC, unless the sequence is set explicitly as shown above.

  • GnuCOBOL is often presented as a drop-in way to practice mainframe COBOL; for national and UTF-8 data in 3.2 it is not one, as the table above shows.