"""cobol-reconcile-two-files.py - compute the report cobol-reconcile-two-files.cob should write. Reads the same two files the COBOL program reads, checks their layout and their sort order, runs the same balance-line match (HIGH-VALUES as the end-of-file key), and prints recon.txt line by line with the same edited pictures: ZZZ,ZZZ,ZZ9.99 BLANK WHEN ZERO bank and book amounts on a detail line -,---,---,--9.99 BLANK WHEN ZERO the difference on a detail line (floating minus) ZZ,ZZ9 the four counts -,---,---,---,--9.99 the three money totals (floating minus) Lines are printed without trailing blanks. Standard library only; Python 3.8 or later. Run from the site folder: python code/cobol-reconcile-two-files.py """ from decimal import Decimal import sys BANK = "datasets/cobol-reconcile-two-files-bank.dat" BOOK = "datasets/cobol-reconcile-two-files.dat" HIGH = "￿" * 8 # stands in for HIGH-VALUES: sorts above every digit string def load(path): recs, prev = [], "" for n, line in enumerate(open(path, encoding="ascii").read().split("\n"), 1): if not line: continue assert len(line) == 47, f"{path} line {n}: {len(line)} bytes, expected 47" ref, date, amt, desc = line[0:8], line[8:16], line[16:27], line[27:47] assert ref.isdigit() and date.isdigit() and amt.isdigit(), f"{path} line {n}: non-digit in a numeric field" if ref <= prev: sys.exit(f"SEQUENCE ERROR IN {path} AT {ref} (the COBOL program would stop with return code 16)") prev = ref recs.append((ref, int(amt), date, desc.rstrip())) return recs def z_amount(cents): # ZZZ,ZZZ,ZZ9.99 BLANK WHEN ZERO, 14 characters return " " * 14 if cents == 0 else f"{Decimal(cents) / 100:,.2f}".rjust(14) def floating_minus(cents, width): # the sign lands immediately left of the first digit s = f"{Decimal(abs(cents)) / 100:,.2f}" return (("-" + s) if cents < 0 else s).rjust(width) def z_diff(cents): # -,---,---,--9.99 BLANK WHEN ZERO, 16 characters return " " * 16 if cents == 0 else floating_minus(cents, 16) def z_count(n): # ZZ,ZZ9, 6 characters return f"{n:,}".rjust(6) def z_money(cents): # -,---,---,---,--9.99, 20 characters return floating_minus(cents, 20) def detail(status, ref, bank, book, diff): return status.ljust(14) + " " + ref + " " + z_amount(bank) + " " + z_amount(book) + " " + z_diff(diff) bank, book = load(BANK), load(BOOK) out = ["RECONCILIATION: BANK FILE AGAINST BOOK FILE", "STATUS".ljust(16) + "REFERENCE".ljust(10) + " BANK AMOUNT" + " " + " BOOK AMOUNT" + " " + " DIFFERENCE"] counts = dict(matched=0, differs=0, bank_only=0, book_only=0) diffs = [] i = j = 0 bkey = bank[0][0] if bank else HIGH kkey = book[0][0] if book else HIGH while not (bkey == HIGH and kkey == HIGH): if bkey == kkey: d = bank[i][1] - book[j][1] status = "MATCHED" if d == 0 else "AMOUNT DIFFERS" counts["matched" if d == 0 else "differs"] += 1 out.append(detail(status, bkey, bank[i][1], book[j][1], d)) diffs.append(d) i += 1 j += 1 elif bkey < kkey: d = bank[i][1] counts["bank_only"] += 1 out.append(detail("BANK ONLY", bkey, bank[i][1], 0, d)) diffs.append(d) i += 1 else: d = -book[j][1] counts["book_only"] += 1 out.append(detail("BOOK ONLY", kkey, 0, book[j][1], d)) diffs.append(d) j += 1 bkey = bank[i][0] if i < len(bank) else HIGH kkey = book[j][0] if j < len(book) else HIGH bank_total = sum(r[1] for r in bank) book_total = sum(r[1] for r in book) out.append("") for label, key in (("MATCHED", "matched"), ("AMOUNT DIFFERS", "differs"), ("BANK ONLY", "bank_only"), ("BOOK ONLY", "book_only")): out.append(label.ljust(24) + z_count(counts[key])) out.append("BANK FILE TOTAL".ljust(24) + z_money(bank_total)) out.append("BOOK FILE TOTAL".ljust(24) + z_money(book_total)) out.append("NET DIFFERENCE".ljust(24) + z_money(bank_total - book_total)) for line in out: print(line.rstrip()) # The control-total identity the report rests on: every reference is counted once, and the net # difference of the two file totals is exactly the sum of the per-reference differences. assert sum(counts.values()) == len({r[0] for r in bank} | {r[0] for r in book}) assert bank_total - book_total == sum(diffs) print(f"\nstdout: MATCHED {counts['matched']:05d} DIFFERS {counts['differs']:05d} " f"BANK ONLY {counts['bank_only']:05d} BOOK ONLY {counts['book_only']:05d}")