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:
- A table showing each branch's monthly deposits for the year, with a year-to-date total
- A ranked list of branches by total deposits (highest to lowest)
- The network-wide average for each month
- 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
-
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?
-
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?
-
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.
-
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?
-
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.
-
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?
-
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?