"""cobol-bank-statement-batch-job.py - the statement and rejects that cobol-bank-statement-batch-job.cob should write. Reads datasets/cobol-bank-statement-batch-job.dat with the copybook's layout (72-byte records: account 9(8), date 9(8), type X, amount 9(9)V99, description X(40), filler X(4)) and applies the program's rules: the four validity checks in the same order (account, date and amount must be all digits, the type must be D or C), a control break on the account, and the same edited pictures (Z,ZZZ,ZZZ,ZZ9.99 for amounts, -,---,---,--9.99 with a floating minus for balances, ZZZ,ZZ9 for counts, 9(4)/9(2)/9(2) for dates). Standard library only. python code/cobol-bank-statement-batch-job.py prints the expected statement.txt, then the expected rejects.dat (trailing spaces trimmed) python code/cobol-bank-statement-batch-job.py statement.txt rejects.dat also compares the files the COBOL program wrote with the expected lines, ignoring trailing spaces, prints any line that differs, and exits 1 if one does Run it from the site folder, like the program, so the relative path to the data file resolves. """ import os import sys DAT = os.path.join("datasets", "cobol-bank-statement-batch-job.dat") def z_amount(cents: int) -> str: """PIC Z,ZZZ,ZZZ,ZZ9.99: 16 characters, leading zeros suppressed, no sign.""" return f"{abs(cents) / 100:,.2f}".rjust(16) def floating_minus(cents: int) -> str: """PIC -,---,---,--9.99: 16 characters, a minus against the first digit only when negative.""" text = f"{abs(cents) / 100:,.2f}" return ("-" + text if cents < 0 else text).rjust(16) def z_count(n: int) -> str: """PIC ZZZ,ZZ9: 7 characters.""" return f"{n:,}".rjust(7) def is_numeric(s: str) -> bool: """The NUMERIC class test on an unsigned DISPLAY item: every character a digit.""" return s != "" and all(c in "0123456789" for c in s) def build(path: str = DAT): stmt = ["BANK STATEMENT"] rejects = [] counts = {"read": 0, "accepted": 0, "rejected": 0, "accounts": 0} grand_dr = grand_cr = 0 current = None balance = acct_dr = acct_cr = 0 def account_break(): stmt.append(" Debits " + z_amount(acct_dr) + " Credits " + z_amount(acct_cr)) stmt.append(" Closing balance" + " " * 48 + floating_minus(balance)) with open(path, encoding="ascii", newline="") as f: for raw in f: rec = raw.rstrip("\r\n").ljust(72)[:72] counts["read"] += 1 account, date, kind, amount, desc = rec[0:8], rec[8:16], rec[16], rec[17:28], rec[28:68] reason = "" if not is_numeric(account): reason = "ACCOUNT NOT NUMERIC" elif not is_numeric(date): reason = "DATE NOT NUMERIC" elif not is_numeric(amount): reason = "AMOUNT NOT NUMERIC" elif kind not in ("D", "C"): reason = "UNKNOWN TYPE" if reason: counts["rejected"] += 1 rejects.append(rec + " | " + reason.ljust(30)) continue if current != account: if current is not None: account_break() current = account counts["accounts"] += 1 balance = acct_dr = acct_cr = 0 stmt.append("") stmt.append("Account " + account) stmt.append(" " + "Date".ljust(10) + " T " + "Description".ljust(30) + " " + "Amount".rjust(16) + " " + "Balance".rjust(16)) counts["accepted"] += 1 cents = int(amount) if kind == "C": balance += cents acct_cr += cents grand_cr += cents else: balance -= cents acct_dr += cents grand_dr += cents stmt.append(" " + f"{date[0:4]}/{date[4:6]}/{date[6:8]}" + " " + kind + " " + desc[:30] + " " + z_amount(cents) + " " + floating_minus(balance)) if current is not None: account_break() stmt.append("") stmt.append("Accounts :" + z_count(counts["accounts"])) stmt.append("Transactions accepted:" + z_count(counts["accepted"])) stmt.append("Transactions rejected:" + z_count(counts["rejected"])) stmt.append("Total debits :" + z_amount(grand_dr)) stmt.append("Total credits :" + z_amount(grand_cr)) return [s.rstrip() for s in stmt], [r.rstrip() for r in rejects], counts def compare(name, expected, path) -> int: with open(path, encoding="ascii", errors="replace", newline="") as f: got = [line.rstrip("\r\n").rstrip() for line in f] bad = 0 for i in range(max(len(expected), len(got))): e = expected[i] if i < len(expected) else "" g = got[i] if i < len(got) else "" if e != g: bad += 1 print(f"{name} line {i + 1}:\n expected {e!r}\n got {g!r}") print(f"{name}: {len(got)} lines, {bad} differ") return bad def main(argv) -> int: stmt, rejects, counts = build() print("\n".join(stmt)) print("--- rejects.dat ---") print("\n".join(rejects)) print(f"--- read {counts['read']}, accepted {counts['accepted']}, rejected {counts['rejected']}, " f"accounts {counts['accounts']} ---") if len(argv) == 2: bad = compare("statement.txt", stmt, argv[0]) + compare("rejects.dat", rejects, argv[1]) return 1 if bad else 0 return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))