"""python-fixed-width-test-data-cobol.py Generate a deterministic fixed-width transaction file for the COBOL reader in python-fixed-width.cob, then parse the file back and print the same control totals the COBOL program prints. Record layout (64 bytes per line, LF line ending, matches the copybook code/python-fixed-width-test-data-cobol.cpy): cols 1- 8 Txn-Id PIC 9(8) zero padded cols 9-14 Txn-Account PIC 9(6) zero padded cols 15-16 Txn-Type PIC X(2) "DR" or "CR" cols 17-27 Txn-Amount PIC 9(9)V99 11 digits, implied decimal point (cents) cols 28-35 Txn-Date PIC 9(8) YYYYMMDD cols 36-64 Txn-Memo PIC X(29) left justified, space padded Inputs : none (random.seed(20260905) makes the file reproducible) Outputs: datasets/python-fixed-width-test-data-cobol.dat and control totals on stdout Run : python code/python-fixed-width-test-data-cobol.py (from the site folder) Standard library only. """ from __future__ import annotations import os import random import struct import sys ROWS = 200 RECORD_LEN = 64 OUT = os.path.join("datasets", "python-fixed-width-test-data-cobol.dat") ACCOUNTS = [100234, 100235, 100240, 100251, 100262, 100270, 100281, 100299] MEMOS = ["Card purchase", "Standing order", "Salary", "Refund", "Direct debit", "Cash withdrawal", "Transfer in", "Transfer out", "Interest", "Fee"] # struct format: 8s 6s 2s 11s 8s 29s = 64 bytes; "s" fields are raw bytes LAYOUT = struct.Struct("8s6s2s11s8s29s") assert LAYOUT.size == RECORD_LEN def make_record(n: int, rng: random.Random) -> str: txn_id = f"{n:08d}" account = f"{rng.choice(ACCOUNTS):06d}" txn_type = rng.choice(["DR", "DR", "CR"]) cents = rng.randint(1, 250_000_00) # 0.01 .. 250,000.00 amount = f"{cents:011d}" # PIC 9(9)V99: no point, cents included day = rng.randint(1, 28) month = rng.randint(1, 9) date = f"2026{month:02d}{day:02d}" memo = rng.choice(MEMOS)[:29].ljust(29) # PIC X(29): space padded on the right rec = txn_id + account + txn_type + amount + date + memo assert len(rec) == RECORD_LEN, len(rec) return rec def write_file(path: str) -> None: rng = random.Random(20260905) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w", encoding="ascii", newline="\n") as fh: # LF only, like a Unix line sequential file for n in range(1, ROWS + 1): fh.write(make_record(n, rng) + "\n") def parse_file(path: str) -> dict: totals = {"read": 0, "dr_count": 0, "cr_count": 0, "bad": 0, "dr_total": 0, "cr_total": 0} with open(path, "rb") as fh: for raw in fh: line = raw.rstrip(b"\n") if len(line) != RECORD_LEN: raise ValueError(f"record of {len(line)} bytes, expected {RECORD_LEN}") txn_id, account, txn_type, amount, date, memo = LAYOUT.unpack(line) totals["read"] += 1 if not amount.isdigit(): # the COBOL program's IS NUMERIC test totals["bad"] += 1 continue cents = int(amount) # implied decimal point: value is cents if txn_type == b"DR": totals["dr_count"] += 1 totals["dr_total"] += cents elif txn_type == b"CR": totals["cr_count"] += 1 totals["cr_total"] += cents else: totals["bad"] += 1 return totals def money(cents: int, width: int = 16) -> str: """Render like COBOL PIC Z,ZZZ,ZZZ,ZZ9.99 (16 chars, leading zeros suppressed).""" return f"{cents / 100:,.2f}".rjust(width) def signed_money(cents: int, width: int = 16) -> str: """Render like COBOL PIC -,ZZZ,ZZZ,ZZ9.99: one minus is a FIXED sign in the leftmost position (a space when not negative), then the zero-suppressed digits (16 chars in all).""" return ("-" if cents < 0 else " ") + f"{abs(cents) / 100:,.2f}".rjust(width - 1) def main() -> int: write_file(OUT) t = parse_file(OUT) print(f"Wrote {ROWS} records of {RECORD_LEN} bytes to {OUT}") print("Expected COBOL output:") print(f"Records read : {t['read']:>7,}") print(f"Debits : {t['dr_count']:>7,} {money(t['dr_total'])}") print(f"Credits : {t['cr_count']:>7,} {money(t['cr_total'])}") print(f"Rejected : {t['bad']:>7,}") print(f"Net (CR - DR) : {signed_money(t['cr_total'] - t['dr_total'])}") return 0 if __name__ == "__main__": sys.exit(main())