"""cobol-write-report-file-page-breaks.py — write the fixed-width transaction file the COBOL report program reads, then build the report the COBOL program should produce and print its arithmetic: detail lines per page, page count, per-page subtotals and the grand totals. The point of this script is verification. The COBOL listing in cobol-write-report.cob was desk-checked, not compiled, so the numbers quoted in the article come from here: the same page arithmetic and the same edited pictures, written a second time in a language that runs. Input record layout (64 bytes per record, LF line endings, no header row; matches the copybook code/cobol-write-report-file-page-breaks.cpy): cols 1- 8 Txn-Date PIC 9(8) YYYYMMDD cols 9-14 Txn-Ref PIC X(6) reference, T00001 upwards cols 15-17 Txn-Branch PIC X(3) branch code cols 18-45 Txn-Desc PIC X(28) description, space padded cols 46-56 Txn-Amount PIC 9(9)V99 eleven digits, implied decimal point cols 57 Txn-DC PIC X D = debit, C = credit cols 58-64 FILLER PIC X(7) spaces Page model (identical to the COBOL program's constants): PAGE_SIZE 30 usable lines, HEADER 5 lines, FOOTER 2 lines => 23 detail lines per page; the summary block starts a fresh page. Run from the site folder: python code/cobol-write-report-file-page-breaks.py (numbers) python code/cobol-write-report-file-page-breaks.py --print-report (the report) Standard library only (Python 3.8+). """ import os import random import sys RECORD_LEN = 64 LINE_LEN = 72 ROWS = 47 SEED = 20260907 PAGE_SIZE = 30 # usable lines on one logical page HEADER_LINES = 5 # title, run stamp, blank, column headings, rule FOOTER_LINES = 2 # rule, page total SUMMARY_LINES = 7 # blank, rule, records, debits, credits, net, pages DETAILS_PER_PAGE = PAGE_SIZE - HEADER_LINES - FOOTER_LINES TITLE = "DAILY TRANSACTION REGISTER" RUN_DATE = "2026/09/07" # the COBOL takes these from FUNCTION CURRENT-DATE RUN_TIME = "07:52:31" BRANCHES = ["BRI", "CRK", "GAL", "LMK"] DESCRIPTIONS = [ "CARD PAYMENT", "STANDING ORDER", "SALARY CREDIT", "SUPPLIER INVOICE", "DIRECT DEBIT", "CASH LODGEMENT", "TRANSFER IN", "TRANSFER OUT", "INTEREST APPLIED", "ACCOUNT FEE", "REFUND ISSUED", "CHEQUE PRESENTED", ] # ----------------------------------------------------------------- the file -- def make_record(n, rng): day = 1 + (n - 1) % 5 # 2026-09-01 .. 2026-09-05 date = f"202609{day:02d}" ref = f"T{n:05d}" branch = rng.choice(BRANCHES) desc = rng.choice(DESCRIPTIONS)[:28].ljust(28) cents = rng.randint(100, 999999) # 1.00 .. 9,999.99 dc = "D" if rng.random() < 0.62 else "C" rec = f"{date}{ref}{branch}{desc}{cents:011d}{dc}{'':7}" assert len(rec) == RECORD_LEN, (len(rec), rec) return rec, dict(date=date, ref=ref, branch=branch, desc=desc.rstrip(), cents=cents, dc=dc) def build_rows(): rng = random.Random(SEED) lines, rows = [], [] for n in range(1, ROWS + 1): rec, row = make_record(n, rng) lines.append(rec) rows.append(row) return lines, rows def write_data_file(lines): here = os.path.dirname(os.path.abspath(__file__)) out = os.path.join(here, os.pardir, "datasets", "cobol-write-report-file-page-breaks.dat") with open(out, "w", encoding="ascii", newline="\n") as fh: for line in lines: fh.write(line + "\n") return os.path.normpath(out) # ------------------------------------------------------- the edited pictures -- def pic_amount(cents): """PIC Z,ZZZ,ZZ9.99 — twelve characters, leading zeros suppressed.""" whole, frac = divmod(abs(cents), 100) return f"{whole:,}.{frac:02d}".rjust(12) def pic_amount_cr(cents): """PIC Z,ZZZ,ZZ9.99CR — fourteen characters; CR only when negative.""" return pic_amount(cents) + ("CR" if cents < 0 else " ") def pic_count(n): """PIC ZZ,ZZ9 — six characters.""" return f"{n:,}".rjust(6) def pic_date(yyyymmdd): """PIC 9(4)/9(2)/9(2) — ten characters.""" return f"{yyyymmdd[0:4]}/{yyyymmdd[4:6]}/{yyyymmdd[6:8]}" def check(line): assert len(line) == LINE_LEN, (len(line), line) return line # ------------------------------------------------------------- the report ---- class Report: def __init__(self): self.lines = [] # (page, line_on_page, text) self.page_no = 0 self.page_line = 0 self.page_total = 0 self.page_totals = [] self.detail_count = 0 def put(self, text): self.page_line += 1 self.lines.append((self.page_no, self.page_line, check(text))) def start_page(self, summary=False): self.page_no += 1 self.page_line = 0 self.page_total = 0 self.put(" " + TITLE.ljust(26) + " " * 28 + "PAGE " + str(self.page_no).rjust(3) + " " * 8) self.put(" RUN " + RUN_DATE + " " + RUN_TIME + " " * 46) self.put(" " * LINE_LEN) if summary: self.put(" " + "REPORT SUMMARY".ljust(70)) else: self.put(" " + "REF".ljust(6) + " " + "DATE".ljust(10) + " " + "BR".ljust(3) + " " + "DESCRIPTION".ljust(28) + " " + "AMOUNT".rjust(12) + " T ") self.put("-" * LINE_LEN) def end_page(self): self.put("-" * LINE_LEN) self.put(" " + " " * 38 + "PAGE " + str(self.page_no).rjust(3) + " TOTAL " + " " + pic_amount(self.page_total) + " " * 3) self.page_totals.append((self.page_no, self.page_total)) def room_for(self, wanted): return self.page_line + wanted + FOOTER_LINES <= PAGE_SIZE def detail(self, row): if not self.room_for(1): self.end_page() self.start_page() self.put(" " + row["ref"].ljust(6) + " " + pic_date(row["date"]) + " " + row["branch"].ljust(3) + " " + row["desc"].ljust(28) + " " + pic_amount(row["cents"]) + " " + row["dc"] + " ") self.page_total += row["cents"] self.detail_count += 1 def summary(self, read, debits, credits): self.put(" " * LINE_LEN) self.put("-" * LINE_LEN) self.put(" " + "RECORDS READ".ljust(53) + " " + " " * 8 + pic_count(read) + " ") self.put(" " + "DEBIT TOTAL".ljust(53) + " " + pic_amount_cr(debits) + " ") self.put(" " + "CREDIT TOTAL".ljust(53) + " " + pic_amount_cr(credits) + " ") self.put(" " + "NET (DEBITS LESS CREDITS)".ljust(53) + " " + pic_amount_cr(debits - credits) + " ") self.put(" " + "PAGES PRINTED".ljust(53) + " " + " " * 8 + pic_count(self.page_no) + " ") def build_report(rows): rpt = Report() rpt.start_page() breaks = [] for i, row in enumerate(rows, start=1): if not rpt.room_for(1): breaks.append((i, rpt.page_line, rpt.page_no)) rpt.detail(row) rpt.end_page() rpt.start_page(summary=True) debits = sum(r["cents"] for r in rows if r["dc"] == "D") credits = sum(r["cents"] for r in rows if r["dc"] == "C") rpt.summary(len(rows), debits, credits) return rpt, breaks, debits, credits def main(): lines, rows = build_rows() path = write_data_file(lines) rpt, breaks, debits, credits = build_report(rows) if "--print-report" in sys.argv: for _, _, text in rpt.lines: print(text.rstrip()) # the runtime strips trailing spaces too return 0 print(f"wrote {len(lines)} records of {RECORD_LEN} bytes to {path}") print(f"page size {PAGE_SIZE} lines = {HEADER_LINES} header" f" + {DETAILS_PER_PAGE} detail + {FOOTER_LINES} footer") print(f"records read : {len(rows)}") print(f"detail lines/page: {DETAILS_PER_PAGE}") print(f"pages printed : {rpt.page_no}" f" ({len(rpt.page_totals)} detail pages + 1 summary page)") print(f"report lines : {len(rpt.lines)}") print(f"detail lines : {rpt.detail_count}") for n, cents in rpt.page_totals: print(f"page {n} total : {pic_amount(cents).strip()}") print(f"debit total : {pic_amount(debits).strip()}") print(f"credit total : {pic_amount(credits).strip()}") print(f"net (D less C) : {pic_amount_cr(debits - credits).strip()}") for rec_no, line_at, page in breaks: print(f"page break : record {rec_no} arrives with the line" f" counter at {line_at} on page {page};" f" {line_at} + 1 + {FOOTER_LINES} > {PAGE_SIZE}") return 0 if __name__ == "__main__": sys.exit(main())