"""cobol-copybook-copy-replacing.py — companion to the copybook lesson. 1. Reads code/cobol-copybook-copy-replacing.cpy and works out every field's offset and length from its PIC clause, so this script uses the same layout the three COBOL programs bring in, not a second hand-typed copy of it. 2. Writes datasets/cobol-copybook-copy-replacing.dat: 18 account records of 39 bytes, LF line endings, no header. This is the old master ACCT-POST reads. 3. Prints the copybook's data lines as they read after the LEADING replacement ACCT-POST asks for (a plain text substitution on words that begin with the prefix; comment lines are left out). 4. Replays ACCT-POST, ACCT-LIST and ACCT-AUDIT and prints the DISPLAY lines each program should produce. Every label it prints is first checked to exist as a literal in the matching .cob file. Record layout (from the copybook, checked at run time): 1-6 Acct-Id X(6) 7-26 Acct-Name X(20) 27-29 Acct-Branch X(3) 30 Acct-Status X 88 Acct-Open "O", 88 Acct-Closed "C" 31-39 Acct-Balance 9(7)V99 nine digits, implied two decimals Rounding: ROUNDED with no MODE is NEAREST-AWAY-FROM-ZERO in the GnuCOBOL Programmer's Guide (7.6.7); every value here is positive, so that is ROUND_HALF_UP in Python's decimal module. Run from the site folder: python code/cobol-copybook-copy-replacing.py Standard library only (Python 3.8+). The names in the data are invented. """ import os import re from decimal import Decimal, ROUND_HALF_UP HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.normpath(os.path.join(HERE, os.pardir)) SLUG = "cobol-copybook-copy-replacing" CPY = os.path.join(HERE, SLUG + ".cpy") POST_COB = os.path.join(HERE, SLUG + ".cob") LIST_COB = os.path.join(HERE, "cobol-copybook-list.cob") AUDIT_COB = os.path.join(HERE, "cobol-copybook-audit.cob") DAT = os.path.join(ROOT, "datasets", SLUG + ".dat") # (account id, name, branch, status, balance) - last month's master, in id order ACCOUNTS = [ ("100104", "ADEYEMI TOBI", "NTH", "O", "2450.00"), ("100118", "BECKER ANNA", "NTH", "O", "13207.35"), ("100125", "CHAVEZ LUIS", "STH", "O", "0.00"), ("100131", "DUBOIS CLAIRE", "EST", "C", "0.00"), ("100147", "EKSTROM LARS", "STH", "O", "987.62"), ("100152", "FITZGERALD NORA", "NTH", "O", "45000.00"), ("100160", "GUPTA ARJUN", "EST", "O", "1234.50"), ("100173", "HALVORSEN INGRID", "STH", "O", "322.00"), ("100189", "IBARRA SOFIA", "NTH", "C", "0.00"), ("100194", "JANSSEN PIETER", "EST", "O", "7777.77"), ("100206", "KOWALSKI MAREK", "STH", "O", "562.40"), ("100211", "LINDQVIST EMMA", "NTH", "O", "20915.08"), ("100229", "MORAN DECLAN", "EST", "O", "99.99"), ("100235", "NAKAMURA KEN", "STH", "O", "3600.00"), ("100248", "OKAFOR CHIDI", "NTH", "O", "1050.10"), ("100250", "PETROVA ELENA", "EST", "C", "0.00"), ("100263", "QUINTERO MATEO", "STH", "O", "12.34"), ("100277", "RASMUSSEN JONAS", "NTH", "O", "8420.60"), ] MONTHLY_RATE = Decimal("0.0025") # WS-Monthly-Rate PIC V9999 VALUE .0025 CENT = Decimal("0.01") # ------------------------------------------------------------ copybook --- def pic_size(pic): """Character positions a DISPLAY picture occupies: X(20) -> 20, 9(7)V99 -> 9.""" expanded = re.sub(r"(.)\((\d+)\)", lambda m: m.group(1) * int(m.group(2)), pic) return len(expanded.replace("V", "")) def read_copybook(path): """Return (data lines, fields, conditions, record length) from the copybook.""" text = open(path, encoding="ascii").read() data_lines = [ln for ln in text.splitlines() if ln.strip() and not ln.strip().startswith("*>")] fields, conditions, offset = [], {}, 0 last_field = None for ln in data_lines: m = re.match(r"\s*(\d\d)\s+([A-Za-z0-9-]+)\s*(.*)\.\s*$", ln) assert m, f"cannot parse copybook line: {ln!r}" level, name, rest = m.groups() if level == "88": v = re.fullmatch(r'VALUE\s+"(.)"', rest.strip()) assert v, ln conditions[name] = (last_field, v.group(1)) continue p = re.fullmatch(r"PIC\s+(\S+)", rest.strip()) if p is None: assert rest.strip() == "", ln # the 01 group item continue size = pic_size(p.group(1)) fields.append((name, offset, size, p.group(1))) last_field = name offset += size return data_lines, fields, conditions, offset def leading(lines, old, new): """What LEADING ==old== BY ==new== does to these lines' words.""" pat = re.compile(r"(?2}-{off + size:<2} PIC {pic}") old_lines = [lay.build(dict(zip(["Id", "Name", "Branch", "Status", "Balance"], a))) for a in ACCOUNTS] ids = [a[0] for a in ACCOUNTS] assert ids == sorted(ids) and len(set(ids)) == len(ids) with open(DAT, "w", encoding="ascii", newline="\n") as f: for ln in old_lines: f.write(ln + "\n") print(f"wrote {len(old_lines)} records of {length} bytes to {os.path.relpath(DAT, ROOT)}") print("--- copybook data lines after LEADING ==Acct-== BY ==Old-== ---") for ln in leading(data_lines, "Acct-", "Old-"): print(ln) # ------------------------------------------------------- ACCT-POST --- old = [lay.parse(ln) for ln in old_lines] new, credited, interest_total, detail = [], 0, Decimal(0), [] for rec in old: n = dict(rec) # MOVE Old-Record TO New-Record if rec["Status"] == "O" and rec["Balance"] > 0: # IF Old-Open AND Old-Balance > 0 exact = rec["Balance"] * MONTHLY_RATE interest = exact.quantize(CENT, rounding=ROUND_HALF_UP) n["Balance"] = rec["Balance"] + interest interest_total += interest credited += 1 detail.append((rec["Id"], rec["Balance"], exact, interest)) new.append(n) new_lines = [lay.build(n) for n in new] print("--- companion detail: interest per credited account (not a DISPLAY) ---") for acct, bal, exact, interest in detail: print(f" {acct} {ed_money(bal)} x 0.0025 = {exact:>12} -> {ed_money(interest)}") print("--- expected DISPLAY output of ACCT-POST ---") print(label(POST_COB, "ACCT-POST records read ") + ed_count(len(old))) print(label(POST_COB, "ACCT-POST records written ") + ed_count(len(new_lines))) print(label(POST_COB, "ACCT-POST accounts credited") + ed_count(credited)) print(label(POST_COB, "ACCT-POST interest posted ") + ed_money(interest_total)) # ------------------------------------------------------- ACCT-LIST --- print("--- expected DISPLAY output of ACCT-LIST ---") header = "ID".ljust(6) + " " + "NAME".ljust(20) + " " + "BR".ljust(3) + " " + "STATUS" + " " + "BALANCE".rjust(12) print(label(LIST_COB, header)) open_count = closed_count = 0 open_total = Decimal(0) for ln in new_lines: r = lay.parse(ln) if r["Status"] == "O": word = label(LIST_COB, "open") open_count += 1 open_total += r["Balance"] elif r["Status"] == "C": word = label(LIST_COB, "closed") closed_count += 1 else: word = label(LIST_COB, "other") print(f"{r['Id']} {r['Name']} {r['Branch']} {word:<6} {ed_money(r['Balance'])}") print(label(LIST_COB, "Open accounts ") + ed_count(open_count) + label(LIST_COB, " balance ") + ed_money(open_total)) print(label(LIST_COB, "Closed accounts ") + ed_count(closed_count)) # ------------------------------------------------------ ACCT-AUDIT --- print("--- expected DISPLAY output of ACCT-AUDIT ---") pairs = raised = same = problems = 0 old_total = new_total = movement = Decimal(0) for o_ln, n_ln in zip(old_lines, new_lines): o, n = lay.parse(o_ln), lay.parse(n_ln) pairs += 1 old_total += o["Balance"] new_total += n["Balance"] if n["Id"] != o["Id"] or n["Status"] != o["Status"]: problems += 1 if n["Balance"] > o["Balance"]: raised += 1 movement += n["Balance"] - o["Balance"] if n["Status"] == "C": problems += 1 elif n["Balance"] == o["Balance"]: same += 1 else: problems += 1 assert len(old_lines) == len(new_lines) print(label(AUDIT_COB, "ACCT-AUDIT pairs compared ") + ed_count(pairs)) print(label(AUDIT_COB, "ACCT-AUDIT balances raised ") + ed_count(raised)) print(label(AUDIT_COB, "ACCT-AUDIT balances same ") + ed_count(same)) print(label(AUDIT_COB, "ACCT-AUDIT old total ") + ed_money(old_total)) print(label(AUDIT_COB, "ACCT-AUDIT movement ") + ed_money(movement)) print(label(AUDIT_COB, "ACCT-AUDIT new total ") + ed_money(new_total)) assert problems == 0 and old_total + movement == new_total print(label(AUDIT_COB, "ACCT-AUDIT result balanced")) # cross-checks between the three programs' numbers assert movement == interest_total and raised == credited assert open_total == new_total # closed balances are all zero here if __name__ == "__main__": main()