Case Study 1: Branch Performance Dashboard at Evergreen Federal Credit Union

Background

Evergreen Federal Credit Union operates 45 branches across the Pacific Northwest, serving 280,000 members. Each branch reports monthly performance metrics to the central accounting system on the z/OS mainframe: total deposits, total withdrawals, new accounts opened, loans originated, and revenue from fees. These metrics drive executive decisions about branch staffing, expansion, and closure.

The existing reporting system produced a flat listing of all branches sorted by branch code -- 45 lines of numbers with no totals, no comparisons, and no way to answer questions like "Which branches are performing above the regional average?" or "How does Branch 12's January compare to its February?"

Daniel Olsen, a COBOL developer in the IT department, was asked to build a Branch Performance Dashboard: a batch program that loads branch performance data for the entire year into a multi-dimensional table, performs analysis (ranking, averaging, year-to-date accumulation), and produces a formatted report. The program would demonstrate table loading from a file, multi-dimensional OCCURS, SEARCH ALL for binary lookup, and subscript manipulation for cross-dimensional analysis.


The Problem

The input file contains one record per branch per month. By the end of the year, there are up to 540 records (45 branches times 12 months). Each record contains:

  • Branch Code (4 characters, alphanumeric, e.g., "B012")
  • Month Number (2 digits, 01-12)
  • Total Deposits (signed, up to $99,999,999.99)
  • Total Withdrawals (signed, up to $99,999,999.99)
  • New Accounts (unsigned, up to 9,999)
  • Loans Originated (unsigned, up to 999)
  • Fee Revenue (signed, up to $999,999.99)

The dashboard must produce:

  1. A table showing each branch's monthly deposits for the year, with a year-to-date total
  2. A ranked list of branches by total deposits (highest to lowest)
  3. The network-wide average for each month
  4. Identification of branches performing below 75% of the network average (flagged for review)

The Solution

Table Design

The core data structure is a two-dimensional table: 45 branches by 12 months. Each cell contains the five performance metrics. Daniel also needed a one-dimensional branch information table for looking up branch names by code using SEARCH ALL.

       IDENTIFICATION DIVISION.
       PROGRAM-ID.  BRCHPERF.
       AUTHOR.      DANIEL OLSEN.
       DATE-WRITTEN. 2024-07-15.
      *================================================================
      * PROGRAM:  BRCHPERF
      * PURPOSE:  Branch Performance Dashboard for Evergreen
      *           Federal Credit Union. Loads performance data
      *           into a 2-dimensional table (branches x months),
      *           performs analysis, and produces a ranked report.
      *           Demonstrates OCCURS, SEARCH ALL, multi-dim
      *           tables, and table-driven reporting.
      *================================================================

       ENVIRONMENT DIVISION.

       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT PERF-INPUT-FILE
               ASSIGN TO "BRCHDATA"
               ORGANIZATION IS SEQUENTIAL
               FILE STATUS IS WS-INPUT-STATUS.

           SELECT REPORT-OUTPUT-FILE
               ASSIGN TO "RPRTOUT"
               ORGANIZATION IS SEQUENTIAL
               FILE STATUS IS WS-OUTPUT-STATUS.

       DATA DIVISION.

       FILE SECTION.
       FD  PERF-INPUT-FILE
           RECORDING MODE IS F
           RECORD CONTAINS 80 CHARACTERS.
       01  FS-PERF-RECORD.
           05  FS-PR-BRANCH-CODE      PIC X(4).
           05  FS-PR-MONTH            PIC 9(2).
           05  FS-PR-DEPOSITS         PIC S9(8)V99.
           05  FS-PR-WITHDRAWALS      PIC S9(8)V99.
           05  FS-PR-NEW-ACCOUNTS     PIC 9(4).
           05  FS-PR-LOANS            PIC 9(3).
           05  FS-PR-FEE-REVENUE      PIC S9(6)V99.
           05  FILLER                 PIC X(35).

       FD  REPORT-OUTPUT-FILE
           RECORDING MODE IS F
           RECORD CONTAINS 132 CHARACTERS.
       01  FS-REPORT-LINE             PIC X(132).

       WORKING-STORAGE SECTION.

      *----------------------------------------------------------------
      * FILE STATUS
      *----------------------------------------------------------------
       01  WS-INPUT-STATUS            PIC X(2).
           88  INPUT-OK                          VALUE "00".
           88  INPUT-EOF                         VALUE "10".
       01  WS-OUTPUT-STATUS           PIC X(2).
           88  OUTPUT-OK                         VALUE "00".

      *----------------------------------------------------------------
      * BRANCH REFERENCE TABLE
      * Sorted by branch code for SEARCH ALL (binary search).
      * ASCENDING KEY is required for SEARCH ALL.
      *----------------------------------------------------------------
       01  WS-MAX-BRANCHES            PIC 9(3) VALUE 45.

       01  WS-BRANCH-REF-TABLE.
           05  WS-BRANCH-REF-ENTRY
                   OCCURS 45 TIMES
                   ASCENDING KEY IS WS-BR-CODE
                   INDEXED BY WS-BR-IDX.
               10  WS-BR-CODE         PIC X(4).
               10  WS-BR-NAME         PIC X(25).
               10  WS-BR-REGION       PIC X(10).
               10  WS-BR-ACTIVE       PIC X(1).
                   88  BRANCH-ACTIVE             VALUE 'Y'.
                   88  BRANCH-CLOSED             VALUE 'N'.

      *----------------------------------------------------------------
      * MULTI-DIMENSIONAL PERFORMANCE TABLE
      * First dimension: Branch (1-45)
      * Second dimension: Month (1-12)
      * Each cell contains the five performance metrics.
      *----------------------------------------------------------------
       01  WS-PERF-TABLE.
           05  WS-BRANCH-DATA         OCCURS 45 TIMES.
               10  WS-MONTH-DATA      OCCURS 12 TIMES.
                   15  WS-TBL-DEPOSITS
                                      PIC S9(8)V99 COMP-3.
                   15  WS-TBL-WITHDRAWALS
                                      PIC S9(8)V99 COMP-3.
                   15  WS-TBL-NEW-ACCTS
                                      PIC 9(4) COMP-3.
                   15  WS-TBL-LOANS
                                      PIC 9(3) COMP-3.
                   15  WS-TBL-FEES
                                      PIC S9(6)V99 COMP-3.
               10  WS-YTD-DEPOSITS    PIC S9(10)V99 COMP-3.
               10  WS-YTD-NEW-ACCTS   PIC 9(5)      COMP-3.

      *----------------------------------------------------------------
      * NETWORK AVERAGES (per month)
      *----------------------------------------------------------------
       01  WS-NETWORK-AVERAGES.
           05  WS-NET-AVG-ENTRY       OCCURS 12 TIMES.
               10  WS-NET-AVG-DEPOSITS
                                      PIC S9(10)V99 COMP-3.
               10  WS-NET-BRANCH-COUNT
                                      PIC 9(3)      COMP-3.

      *----------------------------------------------------------------
      * RANKING TABLE
      * Holds branch index and YTD deposits for sorting.
      *----------------------------------------------------------------
       01  WS-RANK-TABLE.
           05  WS-RANK-ENTRY          OCCURS 45 TIMES.
               10  WS-RANK-BR-IDX     PIC 9(3) COMP.
               10  WS-RANK-YTD-DEP    PIC S9(10)V99 COMP-3.

      *----------------------------------------------------------------
      * REPORT FORMATTING
      *----------------------------------------------------------------
       01  WS-REPORT-HEADER-1.
           05  FILLER PIC X(40) VALUE
               "EVERGREEN FEDERAL CREDIT UNION          ".
           05  FILLER PIC X(40) VALUE
               "BRANCH PERFORMANCE DASHBOARD            ".
           05  FILLER PIC X(52) VALUE SPACES.

       01  WS-REPORT-HEADER-2.
           05  FILLER PIC X(6) VALUE "BRANCH".
           05  FILLER PIC X(2) VALUE SPACES.
           05  FILLER PIC X(25) VALUE "NAME".
           05  FILLER PIC X(1) VALUE SPACES.
           05  WS-HDR-MONTH           PIC X(10)
                                      OCCURS 12 TIMES.
           05  FILLER PIC X(14) VALUE "     YTD TOTAL".

       01  WS-DETAIL-LINE.
           05  WS-DET-CODE            PIC X(6).
           05  FILLER                 PIC X(2) VALUE SPACES.
           05  WS-DET-NAME            PIC X(25).
           05  FILLER                 PIC X(1) VALUE SPACES.
           05  WS-DET-AMT             PIC Z(7)9.99-
                                      OCCURS 12 TIMES.
           05  FILLER                 PIC X(1) VALUE SPACES.
           05  WS-DET-YTD             PIC Z(9)9.99-.

       01  WS-RANK-LINE.
           05  WS-RANK-NUM            PIC Z9.
           05  FILLER                 PIC X(2) VALUE ". ".
           05  WS-RANK-CODE           PIC X(6).
           05  FILLER                 PIC X(1) VALUE SPACES.
           05  WS-RANK-NAME           PIC X(25).
           05  FILLER                 PIC X(1) VALUE SPACES.
           05  WS-RANK-AMOUNT         PIC $ZZZ,ZZZ,ZZ9.99-.
           05  FILLER                 PIC X(2) VALUE SPACES.
           05  WS-RANK-FLAG           PIC X(15).

      *----------------------------------------------------------------
      * WORK FIELDS
      *----------------------------------------------------------------
       01  WS-BRANCH-SUB             PIC 9(3) COMP.
       01  WS-MONTH-SUB              PIC 9(3) COMP.
       01  WS-SAVE-IDX               PIC 9(3) COMP.
       01  WS-RECORDS-LOADED         PIC S9(7) COMP-3 VALUE 0.
       01  WS-ACTIVE-BRANCHES        PIC 9(3) VALUE 0.
       01  WS-RANK-COUNTER           PIC 9(3).
       01  WS-MONTHLY-TOTAL          PIC S9(10)V99 COMP-3.
       01  WS-NETWORK-AVG            PIC S9(10)V99 COMP-3.
       01  WS-THRESHOLD              PIC S9(10)V99 COMP-3.
       01  WS-DISP-AMT               PIC $ZZZ,ZZZ,ZZ9.99-.
       01  WS-DISP-COUNT             PIC Z,ZZ9.

      *    Temporary fields for bubble sort
       01  WS-TEMP-IDX               PIC 9(3) COMP.
       01  WS-TEMP-YTD               PIC S9(10)V99 COMP-3.
       01  WS-SWAPPED                PIC X(1).
           88  SWAP-OCCURRED                     VALUE 'Y'.
           88  NO-SWAP                           VALUE 'N'.
       01  WS-SORT-I                 PIC 9(3) COMP.
       01  WS-SORT-J                 PIC 9(3) COMP.

       PROCEDURE DIVISION.

       0000-MAIN-CONTROL.
           PERFORM 1000-INITIALIZE
           PERFORM 2000-LOAD-BRANCH-REFERENCE
           PERFORM 3000-LOAD-PERFORMANCE-DATA
           PERFORM 4000-CALCULATE-YTD-TOTALS
           PERFORM 5000-CALCULATE-NETWORK-AVERAGES
           PERFORM 6000-RANK-BRANCHES
           PERFORM 7000-PRODUCE-REPORT
           PERFORM 9000-FINALIZE
           STOP RUN
           .

       1000-INITIALIZE.
           DISPLAY "============================================="
           DISPLAY " BRANCH PERFORMANCE DASHBOARD"
           DISPLAY " EVERGREEN FEDERAL CREDIT UNION"
           DISPLAY "============================================="
           INITIALIZE WS-PERF-TABLE
           INITIALIZE WS-NETWORK-AVERAGES
           INITIALIZE WS-RANK-TABLE

           OPEN INPUT  PERF-INPUT-FILE
                OUTPUT REPORT-OUTPUT-FILE

           IF NOT INPUT-OK
               DISPLAY "ERROR: Cannot open performance data. "
                       "Status: " WS-INPUT-STATUS
               STOP RUN
           END-IF
           .

       2000-LOAD-BRANCH-REFERENCE.
      *    -------------------------------------------------------
      *    Load branch reference data. In production, this would
      *    come from a reference file. For demonstration, we
      *    load a subset of branches with hard-coded data.
      *    The table MUST be in ascending order by WS-BR-CODE
      *    for SEARCH ALL to work correctly.
      *    -------------------------------------------------------
           INITIALIZE WS-BRANCH-REF-TABLE

           MOVE "B001" TO WS-BR-CODE(1)
           MOVE "DOWNTOWN PORTLAND"   TO WS-BR-NAME(1)
           MOVE "METRO"    TO WS-BR-REGION(1)
           SET  BRANCH-ACTIVE(1) TO TRUE

           MOVE "B002" TO WS-BR-CODE(2)
           MOVE "LAKE OSWEGO"         TO WS-BR-NAME(2)
           MOVE "METRO"    TO WS-BR-REGION(2)
           SET  BRANCH-ACTIVE(2) TO TRUE

           MOVE "B003" TO WS-BR-CODE(3)
           MOVE "BEAVERTON CENTRAL"   TO WS-BR-NAME(3)
           MOVE "METRO"    TO WS-BR-REGION(3)
           SET  BRANCH-ACTIVE(3) TO TRUE

           MOVE "B004" TO WS-BR-CODE(4)
           MOVE "SALEM MAIN"          TO WS-BR-NAME(4)
           MOVE "VALLEY"   TO WS-BR-REGION(4)
           SET  BRANCH-ACTIVE(4) TO TRUE

           MOVE "B005" TO WS-BR-CODE(5)
           MOVE "EUGENE GATEWAY"      TO WS-BR-NAME(5)
           MOVE "SOUTH"    TO WS-BR-REGION(5)
           SET  BRANCH-ACTIVE(5) TO TRUE

           MOVE "B006" TO WS-BR-CODE(6)
           MOVE "BEND RIVERSIDE"      TO WS-BR-NAME(6)
           MOVE "EAST"     TO WS-BR-REGION(6)
           SET  BRANCH-ACTIVE(6) TO TRUE

           MOVE "B007" TO WS-BR-CODE(7)
           MOVE "MEDFORD DOWNTOWN"    TO WS-BR-NAME(7)
           MOVE "SOUTH"    TO WS-BR-REGION(7)
           SET  BRANCH-ACTIVE(7) TO TRUE

           MOVE "B008" TO WS-BR-CODE(8)
           MOVE "CORVALLIS CAMPUS"    TO WS-BR-NAME(8)
           MOVE "VALLEY"   TO WS-BR-REGION(8)
           SET  BRANCH-ACTIVE(8) TO TRUE

           MOVE 8 TO WS-ACTIVE-BRANCHES
           .

       3000-LOAD-PERFORMANCE-DATA.
      *    -------------------------------------------------------
      *    Read the performance input file and load each record
      *    into the appropriate cell of the 2D table.
      *    Use SEARCH ALL to find the branch index.
      *    -------------------------------------------------------
           PERFORM 3100-READ-PERF-RECORD

           PERFORM UNTIL INPUT-EOF
      *        Find the branch in the reference table
               SET WS-BR-IDX TO 1
               SEARCH ALL WS-BRANCH-REF-ENTRY
                   AT END
                       DISPLAY "WARNING: Unknown branch code: "
                               FS-PR-BRANCH-CODE
                   WHEN WS-BR-CODE(WS-BR-IDX) =
                        FS-PR-BRANCH-CODE
      *                Convert index to numeric subscript
                       SET WS-BRANCH-SUB TO WS-BR-IDX
                       MOVE FS-PR-MONTH TO WS-MONTH-SUB
      *                Validate month range
                       IF WS-MONTH-SUB >= 1
                           AND WS-MONTH-SUB <= 12
                           PERFORM 3200-STORE-IN-TABLE
                       ELSE
                           DISPLAY "WARNING: Invalid month: "
                                   FS-PR-MONTH
                                   " for branch "
                                   FS-PR-BRANCH-CODE
                       END-IF
               END-SEARCH

               PERFORM 3100-READ-PERF-RECORD
           END-PERFORM

           DISPLAY "  Records loaded: " WS-RECORDS-LOADED
           .

       3100-READ-PERF-RECORD.
           READ PERF-INPUT-FILE
               AT END SET INPUT-EOF TO TRUE
           END-READ
           .

       3200-STORE-IN-TABLE.
      *    Store the performance data in the 2D table cell
      *    addressed by (WS-BRANCH-SUB, WS-MONTH-SUB)
           MOVE FS-PR-DEPOSITS TO
               WS-TBL-DEPOSITS(WS-BRANCH-SUB, WS-MONTH-SUB)
           MOVE FS-PR-WITHDRAWALS TO
               WS-TBL-WITHDRAWALS(WS-BRANCH-SUB, WS-MONTH-SUB)
           MOVE FS-PR-NEW-ACCOUNTS TO
               WS-TBL-NEW-ACCTS(WS-BRANCH-SUB, WS-MONTH-SUB)
           MOVE FS-PR-LOANS TO
               WS-TBL-LOANS(WS-BRANCH-SUB, WS-MONTH-SUB)
           MOVE FS-PR-FEE-REVENUE TO
               WS-TBL-FEES(WS-BRANCH-SUB, WS-MONTH-SUB)
           ADD 1 TO WS-RECORDS-LOADED
           .

       4000-CALCULATE-YTD-TOTALS.
      *    -------------------------------------------------------
      *    For each branch, sum deposits across all 12 months
      *    to produce year-to-date totals.
      *    -------------------------------------------------------
           PERFORM VARYING WS-BRANCH-SUB FROM 1 BY 1
               UNTIL WS-BRANCH-SUB > WS-ACTIVE-BRANCHES
               MOVE ZERO TO WS-YTD-DEPOSITS(WS-BRANCH-SUB)
               MOVE ZERO TO WS-YTD-NEW-ACCTS(WS-BRANCH-SUB)
               PERFORM VARYING WS-MONTH-SUB FROM 1 BY 1
                   UNTIL WS-MONTH-SUB > 12
                   ADD WS-TBL-DEPOSITS(WS-BRANCH-SUB,
                                       WS-MONTH-SUB)
                       TO WS-YTD-DEPOSITS(WS-BRANCH-SUB)
                   ADD WS-TBL-NEW-ACCTS(WS-BRANCH-SUB,
                                        WS-MONTH-SUB)
                       TO WS-YTD-NEW-ACCTS(WS-BRANCH-SUB)
               END-PERFORM
           END-PERFORM
           .

       5000-CALCULATE-NETWORK-AVERAGES.
      *    -------------------------------------------------------
      *    For each month, calculate the network-wide average
      *    deposits across all active branches.
      *    -------------------------------------------------------
           PERFORM VARYING WS-MONTH-SUB FROM 1 BY 1
               UNTIL WS-MONTH-SUB > 12
               MOVE ZERO TO WS-MONTHLY-TOTAL
               MOVE ZERO TO
                   WS-NET-BRANCH-COUNT(WS-MONTH-SUB)
               PERFORM VARYING WS-BRANCH-SUB FROM 1 BY 1
                   UNTIL WS-BRANCH-SUB > WS-ACTIVE-BRANCHES
                   ADD WS-TBL-DEPOSITS(WS-BRANCH-SUB,
                                       WS-MONTH-SUB)
                       TO WS-MONTHLY-TOTAL
                   ADD 1 TO
                       WS-NET-BRANCH-COUNT(WS-MONTH-SUB)
               END-PERFORM
               IF WS-NET-BRANCH-COUNT(WS-MONTH-SUB) > 0
                   DIVIDE WS-MONTHLY-TOTAL
                       BY WS-NET-BRANCH-COUNT(WS-MONTH-SUB)
                       GIVING WS-NET-AVG-DEPOSITS(WS-MONTH-SUB)
                       ROUNDED
                   END-DIVIDE
               END-IF
           END-PERFORM
           .

       6000-RANK-BRANCHES.
      *    -------------------------------------------------------
      *    Load YTD deposits into the ranking table and sort
      *    using a bubble sort (descending order).
      *    In production, the COBOL SORT verb would be used
      *    for larger datasets.
      *    -------------------------------------------------------
      *    Load ranking table
           PERFORM VARYING WS-BRANCH-SUB FROM 1 BY 1
               UNTIL WS-BRANCH-SUB > WS-ACTIVE-BRANCHES
               MOVE WS-BRANCH-SUB TO
                   WS-RANK-BR-IDX(WS-BRANCH-SUB)
               MOVE WS-YTD-DEPOSITS(WS-BRANCH-SUB) TO
                   WS-RANK-YTD-DEP(WS-BRANCH-SUB)
           END-PERFORM

      *    Bubble sort (descending by YTD deposits)
           PERFORM VARYING WS-SORT-I FROM 1 BY 1
               UNTIL WS-SORT-I >= WS-ACTIVE-BRANCHES
               SET NO-SWAP TO TRUE
               COMPUTE WS-SORT-J =
                   WS-ACTIVE-BRANCHES - WS-SORT-I
               PERFORM VARYING WS-BRANCH-SUB FROM 1 BY 1
                   UNTIL WS-BRANCH-SUB > WS-SORT-J
                   COMPUTE WS-SAVE-IDX =
                       WS-BRANCH-SUB + 1
                   IF WS-RANK-YTD-DEP(WS-BRANCH-SUB) <
                      WS-RANK-YTD-DEP(WS-SAVE-IDX)
      *                Swap entries
                       MOVE WS-RANK-BR-IDX(WS-BRANCH-SUB)
                           TO WS-TEMP-IDX
                       MOVE WS-RANK-YTD-DEP(WS-BRANCH-SUB)
                           TO WS-TEMP-YTD
                       MOVE WS-RANK-BR-IDX(WS-SAVE-IDX)
                           TO WS-RANK-BR-IDX(WS-BRANCH-SUB)
                       MOVE WS-RANK-YTD-DEP(WS-SAVE-IDX)
                           TO WS-RANK-YTD-DEP(WS-BRANCH-SUB)
                       MOVE WS-TEMP-IDX
                           TO WS-RANK-BR-IDX(WS-SAVE-IDX)
                       MOVE WS-TEMP-YTD
                           TO WS-RANK-YTD-DEP(WS-SAVE-IDX)
                       SET SWAP-OCCURRED TO TRUE
                   END-IF
               END-PERFORM
               IF NO-SWAP
                   EXIT PERFORM
               END-IF
           END-PERFORM
           .

       7000-PRODUCE-REPORT.
      *    -------------------------------------------------------
      *    Write the dashboard report to the output file.
      *    -------------------------------------------------------
           MOVE WS-REPORT-HEADER-1 TO FS-REPORT-LINE
           WRITE FS-REPORT-LINE
           MOVE SPACES TO FS-REPORT-LINE
           WRITE FS-REPORT-LINE

      *    --- Section 1: Ranked Performance ---
           MOVE "=== BRANCH RANKING BY YTD DEPOSITS ==="
               TO FS-REPORT-LINE
           WRITE FS-REPORT-LINE
           MOVE SPACES TO FS-REPORT-LINE
           WRITE FS-REPORT-LINE

      *    Calculate 75% threshold for flagging
           MOVE ZERO TO WS-MONTHLY-TOTAL
           PERFORM VARYING WS-BRANCH-SUB FROM 1 BY 1
               UNTIL WS-BRANCH-SUB > WS-ACTIVE-BRANCHES
               ADD WS-YTD-DEPOSITS(WS-BRANCH-SUB)
                   TO WS-MONTHLY-TOTAL
           END-PERFORM
           IF WS-ACTIVE-BRANCHES > 0
               DIVIDE WS-MONTHLY-TOTAL
                   BY WS-ACTIVE-BRANCHES
                   GIVING WS-NETWORK-AVG ROUNDED
               END-DIVIDE
           END-IF
           MULTIPLY WS-NETWORK-AVG BY 0.75
               GIVING WS-THRESHOLD ROUNDED
           END-MULTIPLY

           PERFORM VARYING WS-RANK-COUNTER FROM 1 BY 1
               UNTIL WS-RANK-COUNTER > WS-ACTIVE-BRANCHES
               INITIALIZE WS-RANK-LINE
               MOVE WS-RANK-COUNTER TO WS-RANK-NUM
               MOVE WS-RANK-BR-IDX(WS-RANK-COUNTER)
                   TO WS-BRANCH-SUB
               MOVE WS-BR-CODE(WS-BRANCH-SUB)
                   TO WS-RANK-CODE
               MOVE WS-BR-NAME(WS-BRANCH-SUB)
                   TO WS-RANK-NAME
               MOVE WS-RANK-YTD-DEP(WS-RANK-COUNTER)
                   TO WS-RANK-AMOUNT
      *        Flag branches below 75% of average
               IF WS-RANK-YTD-DEP(WS-RANK-COUNTER) <
                  WS-THRESHOLD
                   MOVE "** REVIEW **" TO WS-RANK-FLAG
               ELSE
                   MOVE SPACES TO WS-RANK-FLAG
               END-IF
               MOVE WS-RANK-LINE TO FS-REPORT-LINE
               WRITE FS-REPORT-LINE
           END-PERFORM

      *    --- Network Average Line ---
           MOVE SPACES TO FS-REPORT-LINE
           WRITE FS-REPORT-LINE
           MOVE WS-NETWORK-AVG TO WS-DISP-AMT
           STRING "    NETWORK AVERAGE YTD: "
                       DELIMITED BY SIZE
                  WS-DISP-AMT DELIMITED BY SIZE
               INTO FS-REPORT-LINE
           END-STRING
           WRITE FS-REPORT-LINE
           MOVE WS-THRESHOLD TO WS-DISP-AMT
           STRING "    75% THRESHOLD:       "
                       DELIMITED BY SIZE
                  WS-DISP-AMT DELIMITED BY SIZE
               INTO FS-REPORT-LINE
           END-STRING
           WRITE FS-REPORT-LINE

           DISPLAY "  Report generated successfully."
           .

       9000-FINALIZE.
           CLOSE PERF-INPUT-FILE
                 REPORT-OUTPUT-FILE
           DISPLAY " "
           DISPLAY "============================================="
           DISPLAY " DASHBOARD PROCESSING COMPLETE"
           DISPLAY "============================================="
           .

Solution Walkthrough

Multi-Dimensional Table Definition

The heart of the program is the two-dimensional performance table:

       01  WS-PERF-TABLE.
           05  WS-BRANCH-DATA         OCCURS 45 TIMES.
               10  WS-MONTH-DATA      OCCURS 12 TIMES.
                   15  WS-TBL-DEPOSITS
                                      PIC S9(8)V99 COMP-3.

Accessing a cell requires two subscripts: WS-TBL-DEPOSITS(WS-BRANCH-SUB, WS-MONTH-SUB). The first subscript selects the branch, the second selects the month. COBOL stores this in row-major order -- all 12 months for branch 1 are contiguous in memory, followed by all 12 months for branch 2, and so on. This matters for performance: iterating across months for a single branch (which accesses contiguous memory) is faster than iterating across branches for a single month (which accesses memory at regular intervals).

SEARCH ALL for Binary Lookup

When a performance record is read from the input file, the program must find which table row (branch index) corresponds to the branch code. SEARCH ALL performs a binary search:

               SEARCH ALL WS-BRANCH-REF-ENTRY
                   AT END
                       DISPLAY "WARNING: Unknown branch code"
                   WHEN WS-BR-CODE(WS-BR-IDX) =
                        FS-PR-BRANCH-CODE
                       SET WS-BRANCH-SUB TO WS-BR-IDX
               END-SEARCH

For SEARCH ALL to work correctly, two conditions must be met: 1. The table must be defined with ASCENDING KEY IS WS-BR-CODE 2. The table data must actually be in ascending order by that key

The branch reference table is loaded in ascending order by branch code (B001, B002, ..., B008). With 45 branches, a binary search examines at most 6 entries (log2(45) = 5.5), compared to a linear search that examines up to 45 entries -- a significant improvement when performed millions of times.

YTD Calculation: Nested PERFORM VARYING

The year-to-date calculation demonstrates nested iteration over both dimensions:

           PERFORM VARYING WS-BRANCH-SUB FROM 1 BY 1
               UNTIL WS-BRANCH-SUB > WS-ACTIVE-BRANCHES
               MOVE ZERO TO WS-YTD-DEPOSITS(WS-BRANCH-SUB)
               PERFORM VARYING WS-MONTH-SUB FROM 1 BY 1
                   UNTIL WS-MONTH-SUB > 12
                   ADD WS-TBL-DEPOSITS(WS-BRANCH-SUB,
                                       WS-MONTH-SUB)
                       TO WS-YTD-DEPOSITS(WS-BRANCH-SUB)
               END-PERFORM
           END-PERFORM

The outer loop iterates over branches, the inner loop iterates over months. For each branch, all 12 monthly deposit amounts are summed into the YTD total. This pattern -- outer loop over one dimension, inner loop over the other -- is the standard approach for any aggregation across a dimension of a multi-dimensional table.

Ranking: Bubble Sort on a Parallel Table

Rather than sorting the performance table itself (which would be expensive and disruptive to the data layout), Daniel created a separate ranking table containing only the branch index and YTD deposits. The bubble sort rearranges this lightweight table while the main performance table remains intact.

The early-exit optimization (IF NO-SWAP EXIT PERFORM) stops the sort as soon as a pass completes without any swaps, which means the data is fully sorted. For 45 branches, this is adequate. For larger datasets, the COBOL SORT verb would be more appropriate.


Lessons Learned

1. SEARCH ALL Requires Sorted Data -- Always

The ASCENDING KEY clause in the table definition is a promise to the compiler, not an enforcement mechanism. If the data is not actually sorted, SEARCH ALL will produce incorrect results without any error message. Always verify sort order when loading data for binary search.

2. Multi-Dimensional Access Uses Comma-Separated Subscripts

The notation WS-TBL-DEPOSITS(3, 7) accesses branch 3, month 7. This is more readable than nested reference: WS-TBL-DEPOSITS OF WS-MONTH-DATA(7) OF WS-BRANCH-DATA(3).

3. Initialize Tables Before Loading

The INITIALIZE WS-PERF-TABLE statement sets all numeric fields to zero. Without this, any table cell that does not receive data from the input file would contain unpredictable values, corrupting the YTD totals and averages.

4. Separate Ranking from Source Data

By sorting a parallel table of indexes rather than the data table itself, the original data remains available in its natural order. This is essential when multiple report sections need the data in different orders.

5. Memory Considerations for Large Tables

The performance table in this program occupies approximately 45 branches x (12 months x 22 bytes per cell + 8 bytes YTD) = 45 x 272 = 12,240 bytes. Manageable. But if the credit union grew to 500 branches with 24 months of history, the table would be 500 x 544 = 272,000 bytes -- still reasonable for z/OS WORKING-STORAGE but worth monitoring.


Discussion Questions

  1. SEARCH ALL performs a binary search, but the performance data input file is not necessarily sorted by branch code. Why does this not prevent SEARCH ALL from working? What is the difference between the sort order of the reference table and the sort order of the input file?

  2. The program uses a bubble sort to rank branches. With 45 branches, this is acceptable. At what number of branches would you recommend switching to the COBOL SORT verb? What are the trade-offs between an in-memory sort and a SORT verb invocation?

  3. The performance table uses COMP-3 for monetary amounts. If it used DISPLAY format instead, how would the table size change? Calculate the memory difference for 45 branches x 12 months.

  4. The network average calculation divides by the count of active branches. What would happen if WS-ACTIVE-BRANCHES were zero (no branches loaded)? How does the program guard against this? Is the guard sufficient?

  5. The 75% threshold for flagging underperforming branches is hard-coded. How would you make this configurable without recompiling the program? Consider a parameter file, a SYSIN card, or a PARM field in the JCL EXEC statement.

  6. The program loads branch reference data from hard-coded MOVE statements. In production, this would come from a reference file. How would you modify the program to load the branch reference table from a file while maintaining the ascending sort order required by SEARCH ALL?

  7. The table is defined with OCCURS 45 TIMES (a fixed maximum). If the credit union opened a 46th branch, the program would need recompilation. How could OCCURS DEPENDING ON be used to make the branch dimension variable? What additional complications would this introduce?