COBOL Textbook

A COBOL bank-statement batch job: read, validate, total, report

A complete GnuCOBOL batch program that reads sorted transactions, rejects bad records to a file, breaks on account change, and writes a statement with totals.

Published
Updated
Dialect
GnuCOBOL 3.x, free format
Status
Compiled and run with GnuCOBOL 3.2
Length
1,325 words, 6 min

This is the shape of most batch COBOL that has ever run: read a sorted file, check each record, send the bad ones somewhere a person will look, accumulate totals at one or more levels, and write a report. You get the whole thing here as one program of about 200 lines, a twelve-record transaction file with two deliberate faults in it, and a Python script that computes the statement the program is written to produce. The two files it writes, statement.txt and rejects.dat, are the two outputs a bank operations team would actually pick up.

The listing was desk-checked against the GnuCOBOL 3.2 Programmer's Guide, and the expected statement below was produced by running the Python companion, /code/cobol-bank-statement-batch-job.py, which applies the same rules with the same edited layouts. On 29 October 2026 we compiled and ran the program with GnuCOBOL 3.2, and both files it wrote matched the companion's lines exactly. The program leans on one earlier lesson, reading a sequential file to end of file, for the read loop; the WRITE FROM pattern is explained where it appears below.

Record layout

The transaction file has 72 bytes per line and a line feed. The copybook in /code/cobol-bank-statement-batch-job.cpy is the COBOL description; the file is sorted by account and then date, which the control break depends on.

Table 5.1-1
Field Columns PIC Meaning
Txn-Account 1 to 8 9(8) Account number
Txn-Date 9 to 16 9(8) YYYYMMDD
Txn-Type 17 X(1) D debit, C credit
Txn-Amount 18 to 28 9(9)V99 Amount in cents, no point
Txn-Desc 29 to 68 X(40) Description
FILLER 69 to 72 X(4) Unused

Two records are wrong on purpose. Record three carries the amount 00000012.50, with a decimal point where a digit belongs, and record seven has the type X. The program must reject both without touching the totals.

The program

       >>SOURCE FORMAT IS FREE
*> cobol-bank-statement-batch-job.cob — read a sorted transaction file,
*> validate every record, reject the bad ones, and write a statement with
*> one section per account and grand totals at the end
*> Dialect: GnuCOBOL 3.x, free format
*> Status : desk-checked against the GnuCOBOL 3.2 Programmer's Guide;
*>          compiled and run with GnuCOBOL 3.2 on 2026-10-29
*> Inputs : datasets/cobol-bank-statement-batch-job.dat
*>          (LINE SEQUENTIAL, LF line endings, 72 bytes per record,
*>          sorted by account then date)
*> Outputs: statement.txt (the report) and rejects.dat (bad records)
*>          in the current folder; counts on standard output
*> Build  : cobc -x -I code code/cobol-bank-statement-batch-job.cob
*> Run    : ./cobol-bank-statement-batch-job   (from the site folder)
IDENTIFICATION DIVISION.
PROGRAM-ID. BANK-STATEMENT.

ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
    SELECT Txn-File
        ASSIGN TO "datasets/cobol-bank-statement-batch-job.dat"
        ORGANIZATION IS LINE SEQUENTIAL
        FILE STATUS IS WS-Txn-Status.
    SELECT Stmt-File
        ASSIGN TO "statement.txt"
        ORGANIZATION IS LINE SEQUENTIAL
        FILE STATUS IS WS-Stmt-Status.
    SELECT Reject-File
        ASSIGN TO "rejects.dat"
        ORGANIZATION IS LINE SEQUENTIAL
        FILE STATUS IS WS-Reject-Status.

DATA DIVISION.
FILE SECTION.
FD  Txn-File.
COPY "cobol-bank-statement-batch-job.cpy".
FD  Stmt-File.
01  Stmt-Line                   PIC X(100).
FD  Reject-File.
01  Reject-Line                 PIC X(105).

WORKING-STORAGE SECTION.
01  WS-Statuses.
    05  WS-Txn-Status           PIC X(2).
    05  WS-Stmt-Status          PIC X(2).
    05  WS-Reject-Status        PIC X(2).
01  WS-Eof-Flag                 PIC X VALUE "N".
    88  End-Of-File             VALUE "Y".
01  WS-Account-Flag             PIC X VALUE "N".
    88  Account-Open            VALUE "Y".
01  WS-Current-Account          PIC 9(8) VALUE 0.
01  WS-Reason                   PIC X(30).
01  WS-Amounts.
    05  WS-Balance              PIC S9(11)V99 VALUE 0.
    05  WS-Acct-Debits          PIC 9(11)V99 VALUE 0.
    05  WS-Acct-Credits         PIC 9(11)V99 VALUE 0.
    05  WS-Grand-Debits         PIC 9(11)V99 VALUE 0.
    05  WS-Grand-Credits        PIC 9(11)V99 VALUE 0.
01  WS-Counts.
    05  WS-Read-Count           PIC 9(6) VALUE 0.
    05  WS-Accept-Count         PIC 9(6) VALUE 0.
    05  WS-Reject-Count         PIC 9(6) VALUE 0.
    05  WS-Account-Count        PIC 9(6) VALUE 0.

01  WS-Title-Line               PIC X(14) VALUE "BANK STATEMENT".
01  WS-Account-Header.
    05  FILLER                  PIC X(8) VALUE "Account ".
    05  WS-AH-Account           PIC 9(8).
01  WS-Column-Header.
    05  FILLER                  PIC X(2)  VALUE SPACES.
    05  FILLER                  PIC X(10) VALUE "Date".
    05  FILLER                  PIC X(2)  VALUE SPACES.
    05  FILLER                  PIC X(1)  VALUE "T".
    05  FILLER                  PIC X(2)  VALUE SPACES.
    05  FILLER                  PIC X(30) VALUE "Description".
    05  FILLER                  PIC X(1)  VALUE SPACES.
    05  FILLER                  PIC X(16) VALUE "          Amount".
    05  FILLER                  PIC X(1)  VALUE SPACES.
    05  FILLER                  PIC X(16) VALUE "         Balance".
01  WS-Detail-Line.
    05  FILLER                  PIC X(2) VALUE SPACES.
    05  WS-DL-Date              PIC 9(4)/9(2)/9(2).
    05  FILLER                  PIC X(2) VALUE SPACES.
    05  WS-DL-Type              PIC X(1).
    05  FILLER                  PIC X(2) VALUE SPACES.
    05  WS-DL-Desc              PIC X(30).
    05  FILLER                  PIC X(1) VALUE SPACES.
    05  WS-DL-Amount            PIC Z,ZZZ,ZZZ,ZZ9.99.
    05  FILLER                  PIC X(1) VALUE SPACES.
    05  WS-DL-Balance           PIC -,---,---,--9.99.
01  WS-Acct-Summary.
    05  FILLER                  PIC X(11) VALUE "  Debits   ".
    05  WS-AS-Debits            PIC Z,ZZZ,ZZZ,ZZ9.99.
    05  FILLER                  PIC X(11) VALUE "  Credits  ".
    05  WS-AS-Credits           PIC Z,ZZZ,ZZZ,ZZ9.99.
01  WS-Total-Line.
    05  FILLER                  PIC X(17) VALUE "  Closing balance".
    05  FILLER                  PIC X(48) VALUE SPACES.
    05  WS-TL-Balance           PIC -,---,---,--9.99.
01  WS-Grand-Count-Line.
    05  WS-GC-Label             PIC X(22).
    05  WS-GC-Count             PIC ZZZ,ZZ9.
01  WS-Grand-Amount-Line.
    05  WS-GA-Label             PIC X(22).
    05  WS-GA-Amount            PIC Z,ZZZ,ZZZ,ZZ9.99.
01  WS-Reject-Data.
    05  WS-RJ-Record            PIC X(72).
    05  FILLER                  PIC X(3) VALUE " | ".
    05  WS-RJ-Reason            PIC X(30).

PROCEDURE DIVISION.
Main-Paragraph.
    PERFORM Open-Files
    PERFORM Read-Txn
    PERFORM UNTIL End-Of-File
        PERFORM Process-Txn
        PERFORM Read-Txn
    END-PERFORM
    IF Account-Open
        PERFORM Account-Break
    END-IF
    PERFORM Write-Grand-Totals
    CLOSE Txn-File Stmt-File Reject-File
    DISPLAY "Read " WS-Read-Count " accepted " WS-Accept-Count
            " rejected " WS-Reject-Count
            " accounts " WS-Account-Count
    STOP RUN.

Open-Files.
    OPEN INPUT Txn-File
    IF WS-Txn-Status NOT = "00"
        DISPLAY "Cannot open transactions, status " WS-Txn-Status
        STOP RUN
    END-IF
    OPEN OUTPUT Stmt-File Reject-File
    WRITE Stmt-Line FROM WS-Title-Line.

Read-Txn.
    READ Txn-File
        AT END
            SET End-Of-File TO TRUE
    END-READ.

Process-Txn.
    ADD 1 TO WS-Read-Count
    MOVE SPACES TO WS-Reason
    EVALUATE TRUE
        WHEN Txn-Account IS NOT NUMERIC
            MOVE "ACCOUNT NOT NUMERIC" TO WS-Reason
        WHEN Txn-Date IS NOT NUMERIC
            MOVE "DATE NOT NUMERIC" TO WS-Reason
        WHEN Txn-Amount IS NOT NUMERIC
            MOVE "AMOUNT NOT NUMERIC" TO WS-Reason
        WHEN NOT Txn-Type-Valid
            MOVE "UNKNOWN TYPE" TO WS-Reason
    END-EVALUATE
    IF WS-Reason NOT = SPACES
        PERFORM Write-Reject
    ELSE
        IF NOT Account-Open
           OR Txn-Account NOT = WS-Current-Account
            IF Account-Open
                PERFORM Account-Break
            END-IF
            PERFORM Account-Start
        END-IF
        PERFORM Post-Txn
    END-IF.

Account-Start.
    MOVE Txn-Account TO WS-Current-Account
    SET Account-Open TO TRUE
    ADD 1 TO WS-Account-Count
    MOVE 0 TO WS-Balance WS-Acct-Debits WS-Acct-Credits
    MOVE SPACES TO Stmt-Line
    WRITE Stmt-Line
    MOVE WS-Current-Account TO WS-AH-Account
    WRITE Stmt-Line FROM WS-Account-Header
    WRITE Stmt-Line FROM WS-Column-Header.

Post-Txn.
    ADD 1 TO WS-Accept-Count
    IF Txn-Credit
        ADD Txn-Amount TO WS-Balance WS-Acct-Credits WS-Grand-Credits
    ELSE
        SUBTRACT Txn-Amount FROM WS-Balance
        ADD Txn-Amount TO WS-Acct-Debits WS-Grand-Debits
    END-IF
    MOVE Txn-Date TO WS-DL-Date
    MOVE Txn-Type TO WS-DL-Type
    MOVE Txn-Desc TO WS-DL-Desc
    MOVE Txn-Amount TO WS-DL-Amount
    MOVE WS-Balance TO WS-DL-Balance
    WRITE Stmt-Line FROM WS-Detail-Line.

Account-Break.
    MOVE WS-Acct-Debits TO WS-AS-Debits
    MOVE WS-Acct-Credits TO WS-AS-Credits
    WRITE Stmt-Line FROM WS-Acct-Summary
    MOVE WS-Balance TO WS-TL-Balance
    WRITE Stmt-Line FROM WS-Total-Line
    MOVE "N" TO WS-Account-Flag.

Write-Reject.
    ADD 1 TO WS-Reject-Count
    MOVE Txn-Record TO WS-RJ-Record
    MOVE WS-Reason TO WS-RJ-Reason
    WRITE Reject-Line FROM WS-Reject-Data.

Write-Grand-Totals.
    MOVE SPACES TO Stmt-Line
    WRITE Stmt-Line
    MOVE "Accounts             :" TO WS-GC-Label
    MOVE WS-Account-Count TO WS-GC-Count
    WRITE Stmt-Line FROM WS-Grand-Count-Line
    MOVE "Transactions accepted:" TO WS-GC-Label
    MOVE WS-Accept-Count TO WS-GC-Count
    WRITE Stmt-Line FROM WS-Grand-Count-Line
    MOVE "Transactions rejected:" TO WS-GC-Label
    MOVE WS-Reject-Count TO WS-GC-Count
    WRITE Stmt-Line FROM WS-Grand-Count-Line
    MOVE "Total debits         :" TO WS-GA-Label
    MOVE WS-Grand-Debits TO WS-GA-Amount
    WRITE Stmt-Line FROM WS-Grand-Amount-Line
    MOVE "Total credits        :" TO WS-GA-Label
    MOVE WS-Grand-Credits TO WS-GA-Amount
    WRITE Stmt-Line FROM WS-Grand-Amount-Line.
Listing 5.1-1. Lines 1 to 228 of cobol-bank-statement-batch-job.cob.

Line by line

Three SELECT entries name the input and the two outputs. All three are LINE SEQUENTIAL, so the statement and the reject file are ordinary text that any editor can open. Each has its own FILE STATUS item, because a batch job that cannot open its input should say so and stop rather than report zero transactions as if that were a result.

The FILE SECTION holds the input record through the copybook and two plain 01 records for the outputs. Everything the program builds is assembled in WORKING-STORAGE and written with WRITE FROM, which the Programmer's Guide and the IBM reference both define as a MOVE to the record area followed by the WRITE. The report lines are group items made of FILLERs with VALUE clauses and named fields, so each line's layout is visible in the data division rather than scattered through the logic.

The main paragraph is the priming-read pattern. Read-Txn is performed once before the loop, so that the loop condition can be tested against a real record, and again at the end of every iteration. The UNTIL condition is the level-88 End-Of-File, which the AT END branch of READ sets. Without the priming read the loop would process an empty record area on its first pass, which is the source of a great many off-by-one reports.

Process-Txn validates first. An EVALUATE TRUE lists the tests in order of severity and stops at the first that fails; the Programmer's Guide describes the statement as picking the first WHEN whose condition is true. The class tests come from the same guide's rule that only the digits pass NUMERIC, so the record with the decimal point in its amount fails there. The type is checked through the level-88 Txn-Type-Valid, whose VALUE lists both acceptable letters. If a reason was set, the record goes to Write-Reject, which writes the raw 72 bytes followed by a separator and the reason. Nothing about a rejected record reaches the totals.

The control break is the next block. An account change is detected by comparing the record's account with WS-Current-Account; the first record of the file is handled by the Account-Open flag, which is false until the first account starts. On a change, the old account is closed by Account-Break, which writes its debit and credit summary and its closing balance, and then Account-Start resets the per-account accumulators and writes the account header and the column heading. This only works because the file is sorted by account: a second run of account 10002345 later in the file would open a second section for it. The sort order is a precondition of the job, not something the program checks.

Post-Txn applies the transaction. A credit adds to the running balance; a debit subtracts from it; both add to their side's per-account and grand totals in one ADD each, since ADD accepts several receiving fields. The detail line is then built by MOVE. The date goes into a 9(4)/9(2)/9(2) field, which inserts the two slashes exactly as the Programmer's Guide's PICTURE section shows for a date. The 40-character description moves into a 30-character field and is truncated on the right, which is the alphanumeric MOVE rule; the sample descriptions are all shorter than 30, so nothing is lost here. The balance goes into a -,---,---,--9.99 field whose floating minus appears only when the balance is negative, which happens twice in the sample.

Compile and run

python code/cobol-bank-statement-batch-job.py
cobc -x -I code code/cobol-bank-statement-batch-job.cob
./cobol-bank-statement-batch-job
cat statement.txt
cat rejects.dat
python code/cobol-bank-statement-batch-job.py statement.txt rejects.dat

Run from the site folder so that the relative path to the data file resolves. The -I code option lets the compiler find the copybook. The first command prints the expected statement and rejects; the last one compares the two files the program wrote with them, ignoring trailing spaces, and prints any line that differs.

Worked example

The Python run on this machine produced the statement below, with trailing spaces trimmed from each line. The compiled COBOL program wrote the same lines in the same order.

BANK STATEMENT

Account 10002345
  Date        T  Description                              Amount          Balance
  2026/09/01  C  Salary September                       2,500.00         2,500.00
  2026/09/03  D  Card purchase grocer                      45.99         2,454.01
  2026/09/10  D  Rent                                     800.00         1,654.01
  Debits             845.99  Credits          2,500.00
  Closing balance                                                        1,654.01

Account 10002346
  Date        T  Description                              Amount          Balance
  2026/09/02  C  Transfer in                              100.00           100.00
  2026/09/04  D  Cash withdrawal                          150.00           -50.00
  Debits             150.00  Credits            100.00
  Closing balance                                                          -50.00

Account 10002350
  Date        T  Description                              Amount          Balance
  2026/09/01  D  Streaming subscription                    19.99           -19.99
  2026/09/15  C  Refund                                    50.00            30.01
  Debits              19.99  Credits             50.00
  Closing balance                                                           30.01

Account 10002377
  Date        T  Description                              Amount          Balance
  2026/09/07  C  Invoice 4471 paid                     12,345.67        12,345.67
  2026/09/08  D  Supplier payment                       9,999.99         2,345.68
  2026/09/20  D  Bank fee                                 123.45         2,222.23
  Debits          10,123.44  Credits         12,345.67
  Closing balance                                                        2,222.23

Accounts             :      4
Transactions accepted:     10
Transactions rejected:      2
Total debits         :       11,139.42
Total credits        :       14,995.67

The reject file holds the two faulty records with their reasons, and the console line at the end of the run should read Read 000012 accepted 000010 rejected 000002 accounts 000004, with the leading zeros because the counters are unedited PIC 9(6) fields.

1000234520260905D00000012.50Card purchase bad amount                     | AMOUNT NOT NUMERIC
1000234620260906X00000000500Unknown type                                 | UNKNOWN TYPE

Two arithmetic checks tie the report together. The grand totals equal the sum of the four per-account debit and credit lines, and each closing balance equals that account's credits minus its debits, which is what a reader of the statement would check first.

Limitations

The program was compiled and run with GnuCOBOL 3.2 only. In that run, an MSYS2 MinGW build on Windows, the lines of statement.txt came out with their trailing spaces removed and ending in a carriage return and line feed; another build may pad them to the 100-byte record or end them with a line feed alone, so compare the output with trailing spaces trimmed, as the companion does. The program trusts that the file is sorted and does not detect a sequence error; a real job would either sort the file first or test that each account is not lower than the last and reject the run if it is. The validation also accepts any eight digits as a date, so 20261399 passes. There is no page handling, which the report-file lesson covers, and there are no opening balances: every account starts at zero because the sample carries no balance-forward record. Amount fields of 9(11)V99 hold a hundred billion, and the additions to them are unguarded; the lesson on ON SIZE ERROR is where that guard comes from.

Correction, 29 October 2026: the first build command ran python code/cobol-bank-statement-batch-job.py, but that companion script was never shipped. It is now: it reads the data file with the copybook's layout, applies the program's rules and prints the statement and the rejects shown above, and given the two output files it compares them line by line. The same day we compiled and ran the program with GnuCOBOL 3.2; both files matched the output shown here exactly, so the page now says that instead of calling the listing desk-checked only.

Index terms cobol, gnucobol, batch, control-break, validation