Case Study 1: Financial Date Processing Utility
Background
Meridian Capital Markets is a mid-size investment bank that processes fixed-income securities, commercial paper, and foreign exchange transactions. Their back-office settlement system runs on IBM z/OS and consists of twenty-eight COBOL batch programs that calculate settlement dates, accrued interest, maturity schedules, and aging reports.
Until recently, the bank used a hand-written date calculation subroutine -- a 600-line COBOL program called DATECALC that handled leap years, month boundaries, business day adjustments, and holiday calendars. The subroutine had been maintained by the same developer for fifteen years. When that developer retired, the team discovered that DATECALC contained seventeen undocumented special cases, three known bugs that had been worked around by calling programs, and no test suite.
The head of technology decided to replace DATECALC with a new utility built on COBOL intrinsic functions. The new utility would use FUNCTION INTEGER-OF-DATE, FUNCTION DATE-OF-INTEGER, FUNCTION CURRENT-DATE, FUNCTION ANNUITY, FUNCTION MOD, and FUNCTION TEST-DATE-YYYYMMDD to perform all date and financial calculations in a fraction of the code, with the compiler handling leap year logic and calendar arithmetic automatically.
This case study follows the design and implementation of that replacement utility.
Problem Statement
The new date processing utility must support six operations that are fundamental to the bank's settlement and reporting processes:
-
Business Day Calculation -- Given a start date and a number of business days, calculate the resulting date, skipping weekends and holidays from a configurable holiday calendar.
-
Settlement Date Determination -- Given a trade date and a settlement convention (T+1, T+2, T+3), compute the settlement date using business day rules.
-
Accrued Interest Calculation -- Given a bond's last coupon date, next coupon date, settlement date, face value, and coupon rate, calculate the accrued interest using the 30/360 day-count convention.
-
Maturity Date Schedule -- Given an origination date and a term in months, generate a schedule of monthly maturity dates, adjusting for month-end conventions.
-
Aging Report -- Given a list of outstanding invoices with due dates, categorize each into aging buckets (current, 1-30, 31-60, 61-90, over 90 days past due) and compute totals.
-
Loan Payment Calculation -- Given a principal amount, annual interest rate, and term in months, compute the monthly payment using
FUNCTION ANNUITY, and generate a complete amortization schedule with principal and interest breakdown.
Solution Design
The Holiday Calendar
The utility maintains a table of holidays in WORKING-STORAGE. In production, this table would be loaded from a file or database; for this case study, it is hardcoded for the US financial calendar:
01 WS-HOLIDAY-TABLE.
05 WS-HOLIDAY-COUNT PIC 9(03) VALUE 11.
05 WS-HOLIDAYS.
* 2026 US Federal Holidays
10 FILLER PIC 9(08) VALUE 20260101.
10 FILLER PIC 9(08) VALUE 20260119.
10 FILLER PIC 9(08) VALUE 20260216.
10 FILLER PIC 9(08) VALUE 20260525.
10 FILLER PIC 9(08) VALUE 20260703.
10 FILLER PIC 9(08) VALUE 20260704.
10 FILLER PIC 9(08) VALUE 20260907.
10 FILLER PIC 9(08) VALUE 20261012.
10 FILLER PIC 9(08) VALUE 20261111.
10 FILLER PIC 9(08) VALUE 20261126.
10 FILLER PIC 9(08) VALUE 20261225.
05 WS-HOLIDAY-ENTRY REDEFINES WS-HOLIDAYS
OCCURS 11 TIMES.
10 WS-HOLIDAY-DATE PIC 9(08).
Core Program Structure
IDENTIFICATION DIVISION.
PROGRAM-ID. MRDNDATE.
*================================================================*
* MERIDIAN CAPITAL - FINANCIAL DATE PROCESSING UTILITY *
* Replaces legacy DATECALC with intrinsic function-based *
* date arithmetic, settlement calculations, and financial *
* computations. *
*================================================================*
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-CURRENT-DATETIME.
05 WS-CURR-DATE.
10 WS-CURR-YEAR PIC 9(04).
10 WS-CURR-MONTH PIC 9(02).
10 WS-CURR-DAY PIC 9(02).
05 WS-CURR-TIME.
10 WS-CURR-HOUR PIC 9(02).
10 WS-CURR-MIN PIC 9(02).
10 WS-CURR-SEC PIC 9(02).
10 WS-CURR-HUND PIC 9(02).
05 WS-CURR-GMT-OFFSET PIC X(05).
* Date calculation work fields
01 WS-DATE-WORK.
05 WS-INPUT-DATE PIC 9(08).
05 WS-INPUT-DATE-R REDEFINES WS-INPUT-DATE.
10 WS-INPUT-YYYY PIC 9(04).
10 WS-INPUT-MM PIC 9(02).
10 WS-INPUT-DD PIC 9(02).
05 WS-RESULT-DATE PIC 9(08).
05 WS-RESULT-DATE-R REDEFINES WS-RESULT-DATE.
10 WS-RESULT-YYYY PIC 9(04).
10 WS-RESULT-MM PIC 9(02).
10 WS-RESULT-DD PIC 9(02).
05 WS-INTEGER-DATE PIC 9(09).
05 WS-INTEGER-DATE-2 PIC 9(09).
05 WS-DAY-OF-WEEK PIC 9(01).
05 WS-DAYS-DIFF PIC S9(07).
05 WS-BIZ-DAYS-NEEDED PIC 9(05).
05 WS-BIZ-DAYS-COUNTED PIC 9(05).
05 WS-IS-HOLIDAY PIC X VALUE 'N'.
88 DATE-IS-HOLIDAY VALUE 'Y'.
88 DATE-NOT-HOLIDAY VALUE 'N'.
05 WS-IS-BUSINESS-DAY PIC X VALUE 'N'.
88 IS-BIZ-DAY VALUE 'Y'.
88 NOT-BIZ-DAY VALUE 'N'.
05 WS-HOLIDAY-IDX PIC 9(03).
* Holiday table (defined above - shown for context)
01 WS-HOLIDAY-TABLE.
05 WS-HOLIDAY-COUNT PIC 9(03) VALUE 11.
05 WS-HOLIDAYS.
10 FILLER PIC 9(08) VALUE 20260101.
10 FILLER PIC 9(08) VALUE 20260119.
10 FILLER PIC 9(08) VALUE 20260216.
10 FILLER PIC 9(08) VALUE 20260525.
10 FILLER PIC 9(08) VALUE 20260703.
10 FILLER PIC 9(08) VALUE 20260704.
10 FILLER PIC 9(08) VALUE 20260907.
10 FILLER PIC 9(08) VALUE 20261012.
10 FILLER PIC 9(08) VALUE 20261111.
10 FILLER PIC 9(08) VALUE 20261126.
10 FILLER PIC 9(08) VALUE 20261225.
05 WS-HOLIDAY-ENTRY REDEFINES WS-HOLIDAYS
OCCURS 11 TIMES.
10 WS-HOLIDAY-DATE PIC 9(08).
* Settlement work fields
01 WS-SETTLEMENT-WORK.
05 WS-TRADE-DATE PIC 9(08).
05 WS-SETTLE-CONV PIC 9(01).
05 WS-SETTLE-DATE PIC 9(08).
* Accrued interest work fields
01 WS-ACCRUED-WORK.
05 WS-AI-LAST-COUPON PIC 9(08).
05 WS-AI-LAST-COUPON-R
REDEFINES WS-AI-LAST-COUPON.
10 WS-AI-LC-YYYY PIC 9(04).
10 WS-AI-LC-MM PIC 9(02).
10 WS-AI-LC-DD PIC 9(02).
05 WS-AI-NEXT-COUPON PIC 9(08).
05 WS-AI-NEXT-COUPON-R
REDEFINES WS-AI-NEXT-COUPON.
10 WS-AI-NC-YYYY PIC 9(04).
10 WS-AI-NC-MM PIC 9(02).
10 WS-AI-NC-DD PIC 9(02).
05 WS-AI-SETTLE-DATE PIC 9(08).
05 WS-AI-SETTLE-DATE-R
REDEFINES WS-AI-SETTLE-DATE.
10 WS-AI-SD-YYYY PIC 9(04).
10 WS-AI-SD-MM PIC 9(02).
10 WS-AI-SD-DD PIC 9(02).
05 WS-AI-FACE-VALUE PIC 9(09)V99.
05 WS-AI-COUPON-RATE PIC 9V9(06).
05 WS-AI-DAYS-ACCRUED PIC 9(05).
05 WS-AI-DAYS-IN-PERIOD PIC 9(05).
05 WS-AI-RESULT PIC S9(09)V99.
* Loan amortization work fields
01 WS-LOAN-WORK.
05 WS-LN-PRINCIPAL PIC 9(09)V99.
05 WS-LN-ANNUAL-RATE PIC 9(02)V9(06).
05 WS-LN-MONTHLY-RATE PIC 9V9(08).
05 WS-LN-TERM-MONTHS PIC 9(04).
05 WS-LN-PAYMENT PIC 9(07)V99.
05 WS-LN-REMAINING-BAL PIC S9(09)V99.
05 WS-LN-INTEREST-PORTION PIC 9(07)V99.
05 WS-LN-PRINCIPAL-PORTION PIC 9(07)V99.
05 WS-LN-TOTAL-INTEREST PIC 9(09)V99.
05 WS-LN-TOTAL-PAID PIC 9(09)V99.
05 WS-LN-MONTH-CTR PIC 9(04).
05 WS-LN-PAYMENT-DATE PIC 9(08).
* Aging report work fields
01 WS-AGING-WORK.
05 WS-AG-DUE-DATE PIC 9(08).
05 WS-AG-TODAY PIC 9(08).
05 WS-AG-DAYS-PAST PIC S9(05).
05 WS-AG-AMOUNT PIC 9(09)V99.
01 WS-AGING-BUCKETS.
05 WS-AG-CURRENT PIC S9(11)V99 VALUE 0.
05 WS-AG-1-30 PIC S9(11)V99 VALUE 0.
05 WS-AG-31-60 PIC S9(11)V99 VALUE 0.
05 WS-AG-61-90 PIC S9(11)V99 VALUE 0.
05 WS-AG-OVER-90 PIC S9(11)V99 VALUE 0.
05 WS-AG-TOTAL PIC S9(11)V99 VALUE 0.
* Invoice table for aging demonstration
01 WS-INVOICE-TABLE.
05 WS-INV-COUNT PIC 9(03) VALUE 8.
05 WS-INVOICES.
10 FILLER PIC X(30) VALUE '1001 20260115 000012500.00'.
10 FILLER PIC X(30) VALUE '1002 20260125 000008750.50'.
10 FILLER PIC X(30) VALUE '1003 20251215 000045000.00'.
10 FILLER PIC X(30) VALUE '1004 20251110 000003200.75'.
10 FILLER PIC X(30) VALUE '1005 20251001 000067500.00'.
10 FILLER PIC X(30) VALUE '1006 20260201 000015000.00'.
10 FILLER PIC X(30) VALUE '1007 20260208 000022300.00'.
10 FILLER PIC X(30) VALUE '1008 20250801 000009800.25'.
05 WS-INV-ENTRY REDEFINES WS-INVOICES
OCCURS 8 TIMES.
10 WS-INV-NUMBER PIC X(06).
10 WS-INV-DUE-DATE PIC 9(08).
10 FILLER PIC X(02).
10 WS-INV-AMOUNT PIC 9(09)V99.
10 FILLER PIC X(01).
* Formatted output fields
01 WS-FMT-DATE PIC X(10).
01 WS-FMT-AMOUNT PIC $$$,$$$,$$9.99.
01 WS-FMT-RATE PIC Z9.999999.
01 WS-FMT-DAYS PIC Z(4)9.
01 WS-INV-IDX PIC 9(03).
PROCEDURE DIVISION.
0000-MAIN.
MOVE FUNCTION CURRENT-DATE TO WS-CURRENT-DATETIME
DISPLAY '================================================='
DISPLAY ' MERIDIAN CAPITAL - DATE PROCESSING UTILITY'
DISPLAY ' Run Date: ' WS-CURR-YEAR '/'
WS-CURR-MONTH '/' WS-CURR-DAY
' Time: ' WS-CURR-HOUR ':'
WS-CURR-MIN ':' WS-CURR-SEC
DISPLAY '================================================='
DISPLAY SPACES
PERFORM 1000-BUSINESS-DAY-DEMO
PERFORM 2000-SETTLEMENT-DATE-DEMO
PERFORM 3000-ACCRUED-INTEREST-DEMO
PERFORM 4000-MATURITY-SCHEDULE-DEMO
PERFORM 5000-AGING-REPORT-DEMO
PERFORM 6000-LOAN-AMORTIZATION-DEMO
DISPLAY SPACES
DISPLAY '================================================='
DISPLAY ' ALL DEMONSTRATIONS COMPLETE'
DISPLAY '================================================='
STOP RUN.
*================================================================*
* 1000 - BUSINESS DAY CALCULATION *
*================================================================*
1000-BUSINESS-DAY-DEMO.
DISPLAY '--- 1. BUSINESS DAY CALCULATION ---'
DISPLAY SPACES
* Calculate 10 business days from Feb 6, 2026 (Friday)
MOVE 20260206 TO WS-INPUT-DATE
MOVE 10 TO WS-BIZ-DAYS-NEEDED
PERFORM 1100-ADD-BUSINESS-DAYS
DISPLAY ' Start Date: 2026/02/06 (Friday)'
DISPLAY ' Business Days: 10'
MOVE WS-RESULT-DATE TO WS-INPUT-DATE
DISPLAY ' Result Date: '
WS-RESULT-YYYY '/' WS-RESULT-MM '/'
WS-RESULT-DD
DISPLAY SPACES.
1100-ADD-BUSINESS-DAYS.
* Add N business days to WS-INPUT-DATE
* Result goes into WS-RESULT-DATE
IF FUNCTION TEST-DATE-YYYYMMDD(WS-INPUT-DATE)
NOT = 0
DISPLAY ' ERROR: Invalid input date '
WS-INPUT-DATE
MOVE WS-INPUT-DATE TO WS-RESULT-DATE
ELSE
COMPUTE WS-INTEGER-DATE =
FUNCTION INTEGER-OF-DATE(WS-INPUT-DATE)
MOVE 0 TO WS-BIZ-DAYS-COUNTED
PERFORM UNTIL
WS-BIZ-DAYS-COUNTED >= WS-BIZ-DAYS-NEEDED
ADD 1 TO WS-INTEGER-DATE
PERFORM 1200-CHECK-BUSINESS-DAY
IF IS-BIZ-DAY
ADD 1 TO WS-BIZ-DAYS-COUNTED
END-IF
END-PERFORM
COMPUTE WS-RESULT-DATE =
FUNCTION DATE-OF-INTEGER(WS-INTEGER-DATE)
END-IF.
1200-CHECK-BUSINESS-DAY.
* Check if the date in WS-INTEGER-DATE is a business day
* (not a weekend and not a holiday)
SET DATE-NOT-HOLIDAY TO TRUE
SET IS-BIZ-DAY TO TRUE
* Check day of week: MOD 7 gives 0=Sun through 6=Sat
* (depends on epoch; adjust mapping as needed)
COMPUTE WS-DAY-OF-WEEK =
FUNCTION MOD(WS-INTEGER-DATE, 7)
* Sunday = 6, Saturday = 5 for the ANSI epoch
IF WS-DAY-OF-WEEK = 6 OR WS-DAY-OF-WEEK = 5
SET NOT-BIZ-DAY TO TRUE
ELSE
* Check holiday table
COMPUTE WS-RESULT-DATE =
FUNCTION DATE-OF-INTEGER(WS-INTEGER-DATE)
PERFORM VARYING WS-HOLIDAY-IDX
FROM 1 BY 1
UNTIL WS-HOLIDAY-IDX > WS-HOLIDAY-COUNT
OR DATE-IS-HOLIDAY
IF WS-RESULT-DATE =
WS-HOLIDAY-DATE(WS-HOLIDAY-IDX)
SET DATE-IS-HOLIDAY TO TRUE
SET NOT-BIZ-DAY TO TRUE
END-IF
END-PERFORM
END-IF.
*================================================================*
* 2000 - SETTLEMENT DATE DETERMINATION *
*================================================================*
2000-SETTLEMENT-DATE-DEMO.
DISPLAY '--- 2. SETTLEMENT DATE CALCULATION ---'
DISPLAY SPACES
* T+2 settlement for a trade on Friday Feb 6, 2026
MOVE 20260206 TO WS-TRADE-DATE
MOVE 2 TO WS-SETTLE-CONV
MOVE WS-TRADE-DATE TO WS-INPUT-DATE
MOVE WS-SETTLE-CONV TO WS-BIZ-DAYS-NEEDED
PERFORM 1100-ADD-BUSINESS-DAYS
MOVE WS-RESULT-DATE TO WS-SETTLE-DATE
DISPLAY ' Trade Date: 2026/02/06 (Friday)'
DISPLAY ' Convention: T+2'
MOVE WS-SETTLE-DATE TO WS-INPUT-DATE
DISPLAY ' Settlement: '
WS-INPUT-YYYY '/' WS-INPUT-MM '/'
WS-INPUT-DD
' (Tuesday, skips weekend)'
DISPLAY SPACES
* T+1 settlement for a trade on Thursday Feb 12, 2026
MOVE 20260212 TO WS-TRADE-DATE
MOVE 1 TO WS-SETTLE-CONV
MOVE WS-TRADE-DATE TO WS-INPUT-DATE
MOVE WS-SETTLE-CONV TO WS-BIZ-DAYS-NEEDED
PERFORM 1100-ADD-BUSINESS-DAYS
MOVE WS-RESULT-DATE TO WS-SETTLE-DATE
DISPLAY ' Trade Date: 2026/02/12 (Thursday)'
DISPLAY ' Convention: T+1'
MOVE WS-SETTLE-DATE TO WS-INPUT-DATE
DISPLAY ' Settlement: '
WS-INPUT-YYYY '/' WS-INPUT-MM '/'
WS-INPUT-DD
DISPLAY SPACES
* T+2 near Presidents Day holiday (Feb 16, 2026)
MOVE 20260213 TO WS-TRADE-DATE
MOVE 2 TO WS-SETTLE-CONV
MOVE WS-TRADE-DATE TO WS-INPUT-DATE
MOVE WS-SETTLE-CONV TO WS-BIZ-DAYS-NEEDED
PERFORM 1100-ADD-BUSINESS-DAYS
MOVE WS-RESULT-DATE TO WS-SETTLE-DATE
DISPLAY ' Trade Date: 2026/02/13 (Friday)'
DISPLAY ' Convention: T+2 (Presidents Day holiday)'
MOVE WS-SETTLE-DATE TO WS-INPUT-DATE
DISPLAY ' Settlement: '
WS-INPUT-YYYY '/' WS-INPUT-MM '/'
WS-INPUT-DD
' (skips weekend + holiday)'
DISPLAY SPACES.
*================================================================*
* 3000 - ACCRUED INTEREST (30/360 DAY-COUNT) *
*================================================================*
3000-ACCRUED-INTEREST-DEMO.
DISPLAY '--- 3. ACCRUED INTEREST CALCULATION ---'
DISPLAY SPACES
* Bond: $1,000,000 face, 5.25% coupon, semi-annual
* Last coupon: 2025-09-15, Next coupon: 2026-03-15
* Settlement: 2026-02-10
MOVE 20250915 TO WS-AI-LAST-COUPON
MOVE 20260315 TO WS-AI-NEXT-COUPON
MOVE 20260210 TO WS-AI-SETTLE-DATE
MOVE 1000000.00 TO WS-AI-FACE-VALUE
MOVE 0.0525 TO WS-AI-COUPON-RATE
PERFORM 3100-CALC-ACCRUED-INTEREST
DISPLAY ' Face Value: $1,000,000.00'
DISPLAY ' Coupon Rate: 5.25%'
DISPLAY ' Last Coupon: 2025/09/15'
DISPLAY ' Next Coupon: 2026/03/15'
DISPLAY ' Settlement: 2026/02/10'
DISPLAY ' Days Accrued: ' WS-AI-DAYS-ACCRUED
' (30/360 basis)'
DISPLAY ' Days in Period: ' WS-AI-DAYS-IN-PERIOD
MOVE WS-AI-RESULT TO WS-FMT-AMOUNT
DISPLAY ' Accrued Int: ' WS-FMT-AMOUNT
DISPLAY SPACES.
3100-CALC-ACCRUED-INTEREST.
* 30/360 day count convention (US Bond Basis)
* Each month is treated as 30 days, year as 360 days
* Days = (Y2-Y1)*360 + (M2-M1)*30 + (D2-D1)
* where D1 and D2 are adjusted per 30/360 rules
COMPUTE WS-AI-DAYS-ACCRUED =
(WS-AI-SD-YYYY - WS-AI-LC-YYYY) * 360
+ (WS-AI-SD-MM - WS-AI-LC-MM) * 30
+ (WS-AI-SD-DD - WS-AI-LC-DD)
COMPUTE WS-AI-DAYS-IN-PERIOD =
(WS-AI-NC-YYYY - WS-AI-LC-YYYY) * 360
+ (WS-AI-NC-MM - WS-AI-LC-MM) * 30
+ (WS-AI-NC-DD - WS-AI-LC-DD)
* Accrued Interest = Face * (Rate/2) * (Days/Period)
* Divide rate by 2 for semi-annual coupon
COMPUTE WS-AI-RESULT =
WS-AI-FACE-VALUE
* (WS-AI-COUPON-RATE / 2)
* (WS-AI-DAYS-ACCRUED / WS-AI-DAYS-IN-PERIOD).
*================================================================*
* 4000 - MATURITY DATE SCHEDULE *
*================================================================*
4000-MATURITY-SCHEDULE-DEMO.
DISPLAY '--- 4. MATURITY DATE SCHEDULE ---'
DISPLAY SPACES
* Generate 12 monthly dates from origination 2026-01-31
* Demonstrates month-end convention handling
DISPLAY ' Origination: 2026/01/31'
DISPLAY ' Term: 12 months'
DISPLAY ' Schedule:'
DISPLAY ' Month Date Day-of-Week'
DISPLAY ' ----- ---------- -----------'
MOVE 20260131 TO WS-INPUT-DATE
PERFORM VARYING WS-LN-MONTH-CTR
FROM 1 BY 1
UNTIL WS-LN-MONTH-CTR > 12
PERFORM 4100-CALC-MONTHLY-DATE
PERFORM 4200-GET-DAY-NAME
END-PERFORM
DISPLAY SPACES.
4100-CALC-MONTHLY-DATE.
* Calculate the date N months from origination
* Handle month-end convention: if origination is on the
* last day of the month, each future date should also be
* on the last day of its month.
* Calculate target year and month
COMPUTE WS-RESULT-YYYY =
WS-INPUT-YYYY
+ FUNCTION INTEGER-PART(
(WS-INPUT-MM + WS-LN-MONTH-CTR - 1) / 12)
COMPUTE WS-RESULT-MM =
FUNCTION MOD(
(WS-INPUT-MM + WS-LN-MONTH-CTR - 1), 12) + 1
* Find last day of target month using intrinsic functions
* Strategy: go to first of next month, subtract one day
IF WS-RESULT-MM < 12
COMPUTE WS-INTEGER-DATE =
FUNCTION INTEGER-OF-DATE(
WS-RESULT-YYYY * 10000
+ (WS-RESULT-MM + 1) * 100 + 01) - 1
ELSE
COMPUTE WS-INTEGER-DATE =
FUNCTION INTEGER-OF-DATE(
(WS-RESULT-YYYY + 1) * 10000
+ 0101) - 1
END-IF
COMPUTE WS-RESULT-DATE =
FUNCTION DATE-OF-INTEGER(WS-INTEGER-DATE)
* Apply month-end convention: use last day of month
* since origination was Jan 31 (last day of January)
DISPLAY ' ' WS-LN-MONTH-CTR ' '
WS-RESULT-YYYY '/' WS-RESULT-MM '/'
WS-RESULT-DD
WITH NO ADVANCING.
4200-GET-DAY-NAME.
* Determine the day of the week for display
COMPUTE WS-DAY-OF-WEEK =
FUNCTION MOD(WS-INTEGER-DATE, 7)
EVALUATE WS-DAY-OF-WEEK
WHEN 0 DISPLAY ' Monday'
WHEN 1 DISPLAY ' Tuesday'
WHEN 2 DISPLAY ' Wednesday'
WHEN 3 DISPLAY ' Thursday'
WHEN 4 DISPLAY ' Friday'
WHEN 5 DISPLAY ' Saturday'
WHEN 6 DISPLAY ' Sunday'
END-EVALUATE.
*================================================================*
* 5000 - AGING REPORT *
*================================================================*
5000-AGING-REPORT-DEMO.
DISPLAY '--- 5. ACCOUNTS RECEIVABLE AGING ---'
DISPLAY SPACES
MOVE WS-CURR-DATE TO WS-AG-TODAY
INITIALIZE WS-AGING-BUCKETS
DISPLAY ' Aging as of: ' WS-CURR-YEAR '/'
WS-CURR-MONTH '/' WS-CURR-DAY
DISPLAY SPACES
DISPLAY ' Invoice Due Date Amount'
' Days Bucket'
DISPLAY ' ------- ---------- -----------'
'--- ----- ----------'
PERFORM VARYING WS-INV-IDX FROM 1 BY 1
UNTIL WS-INV-IDX > WS-INV-COUNT
PERFORM 5100-AGE-ONE-INVOICE
END-PERFORM
DISPLAY SPACES
DISPLAY ' AGING SUMMARY:'
MOVE WS-AG-CURRENT TO WS-FMT-AMOUNT
DISPLAY ' Current: ' WS-FMT-AMOUNT
MOVE WS-AG-1-30 TO WS-FMT-AMOUNT
DISPLAY ' 1-30 Days: ' WS-FMT-AMOUNT
MOVE WS-AG-31-60 TO WS-FMT-AMOUNT
DISPLAY ' 31-60 Days: ' WS-FMT-AMOUNT
MOVE WS-AG-61-90 TO WS-FMT-AMOUNT
DISPLAY ' 61-90 Days: ' WS-FMT-AMOUNT
MOVE WS-AG-OVER-90 TO WS-FMT-AMOUNT
DISPLAY ' Over 90 Days: ' WS-FMT-AMOUNT
COMPUTE WS-AG-TOTAL =
WS-AG-CURRENT + WS-AG-1-30 + WS-AG-31-60
+ WS-AG-61-90 + WS-AG-OVER-90
MOVE WS-AG-TOTAL TO WS-FMT-AMOUNT
DISPLAY ' TOTAL: ' WS-FMT-AMOUNT
DISPLAY SPACES.
5100-AGE-ONE-INVOICE.
MOVE WS-INV-DUE-DATE(WS-INV-IDX)
TO WS-AG-DUE-DATE
MOVE WS-INV-AMOUNT(WS-INV-IDX)
TO WS-AG-AMOUNT
* Validate the due date before computing
IF FUNCTION TEST-DATE-YYYYMMDD(WS-AG-DUE-DATE) = 0
COMPUTE WS-AG-DAYS-PAST =
FUNCTION INTEGER-OF-DATE(WS-AG-TODAY)
- FUNCTION INTEGER-OF-DATE(WS-AG-DUE-DATE)
ELSE
MOVE 999 TO WS-AG-DAYS-PAST
END-IF
MOVE WS-AG-AMOUNT TO WS-FMT-AMOUNT
MOVE WS-AG-DAYS-PAST TO WS-FMT-DAYS
EVALUATE TRUE
WHEN WS-AG-DAYS-PAST <= 0
ADD WS-AG-AMOUNT TO WS-AG-CURRENT
DISPLAY ' ' WS-INV-NUMBER(WS-INV-IDX)
' ' WS-AG-DUE-DATE
' ' WS-FMT-AMOUNT
' ' WS-FMT-DAYS
' Current'
WHEN WS-AG-DAYS-PAST <= 30
ADD WS-AG-AMOUNT TO WS-AG-1-30
DISPLAY ' ' WS-INV-NUMBER(WS-INV-IDX)
' ' WS-AG-DUE-DATE
' ' WS-FMT-AMOUNT
' ' WS-FMT-DAYS
' 1-30'
WHEN WS-AG-DAYS-PAST <= 60
ADD WS-AG-AMOUNT TO WS-AG-31-60
DISPLAY ' ' WS-INV-NUMBER(WS-INV-IDX)
' ' WS-AG-DUE-DATE
' ' WS-FMT-AMOUNT
' ' WS-FMT-DAYS
' 31-60'
WHEN WS-AG-DAYS-PAST <= 90
ADD WS-AG-AMOUNT TO WS-AG-61-90
DISPLAY ' ' WS-INV-NUMBER(WS-INV-IDX)
' ' WS-AG-DUE-DATE
' ' WS-FMT-AMOUNT
' ' WS-FMT-DAYS
' 61-90'
WHEN OTHER
ADD WS-AG-AMOUNT TO WS-AG-OVER-90
DISPLAY ' ' WS-INV-NUMBER(WS-INV-IDX)
' ' WS-AG-DUE-DATE
' ' WS-FMT-AMOUNT
' ' WS-FMT-DAYS
' Over 90'
END-EVALUATE.
*================================================================*
* 6000 - LOAN AMORTIZATION WITH FUNCTION ANNUITY *
*================================================================*
6000-LOAN-AMORTIZATION-DEMO.
DISPLAY '--- 6. LOAN AMORTIZATION SCHEDULE ---'
DISPLAY SPACES
* $250,000 mortgage at 6.25% for 30 years
MOVE 250000.00 TO WS-LN-PRINCIPAL
MOVE 0.0625 TO WS-LN-ANNUAL-RATE
MOVE 360 TO WS-LN-TERM-MONTHS
COMPUTE WS-LN-MONTHLY-RATE =
WS-LN-ANNUAL-RATE / 12
* Calculate monthly payment using FUNCTION ANNUITY
* ANNUITY(rate, periods) = rate / (1 - (1+rate)^-periods)
* Payment = Principal * ANNUITY(monthly-rate, months)
COMPUTE WS-LN-PAYMENT =
WS-LN-PRINCIPAL
* FUNCTION ANNUITY(
WS-LN-MONTHLY-RATE,
WS-LN-TERM-MONTHS)
MOVE WS-LN-PRINCIPAL TO WS-FMT-AMOUNT
DISPLAY ' Loan Amount: ' WS-FMT-AMOUNT
MOVE WS-LN-ANNUAL-RATE TO WS-FMT-RATE
DISPLAY ' Annual Rate: ' WS-FMT-RATE '%'
DISPLAY ' Term: '
WS-LN-TERM-MONTHS ' months'
MOVE WS-LN-PAYMENT TO WS-FMT-AMOUNT
DISPLAY ' Monthly Payment: ' WS-FMT-AMOUNT
DISPLAY SPACES
* Generate first 12 months of the amortization schedule
DISPLAY ' Month Payment Interest'
' Principal Balance'
DISPLAY ' ----- ---------- ----------'
' ---------- ---------------'
MOVE WS-LN-PRINCIPAL TO WS-LN-REMAINING-BAL
MOVE 0 TO WS-LN-TOTAL-INTEREST
MOVE 0 TO WS-LN-TOTAL-PAID
PERFORM VARYING WS-LN-MONTH-CTR
FROM 1 BY 1
UNTIL WS-LN-MONTH-CTR > 12
PERFORM 6100-CALC-AMORT-LINE
END-PERFORM
DISPLAY SPACES
MOVE WS-LN-TOTAL-INTEREST TO WS-FMT-AMOUNT
DISPLAY ' Year 1 Interest: ' WS-FMT-AMOUNT
MOVE WS-LN-TOTAL-PAID TO WS-FMT-AMOUNT
DISPLAY ' Year 1 Total Paid:' WS-FMT-AMOUNT
MOVE WS-LN-REMAINING-BAL TO WS-FMT-AMOUNT
DISPLAY ' Balance After Y1: ' WS-FMT-AMOUNT
DISPLAY SPACES.
6100-CALC-AMORT-LINE.
* Interest for this month
COMPUTE WS-LN-INTEREST-PORTION =
WS-LN-REMAINING-BAL * WS-LN-MONTHLY-RATE
* Principal for this month
COMPUTE WS-LN-PRINCIPAL-PORTION =
WS-LN-PAYMENT - WS-LN-INTEREST-PORTION
* Update running balance
SUBTRACT WS-LN-PRINCIPAL-PORTION
FROM WS-LN-REMAINING-BAL
* Accumulate totals
ADD WS-LN-INTEREST-PORTION TO WS-LN-TOTAL-INTEREST
ADD WS-LN-PAYMENT TO WS-LN-TOTAL-PAID
DISPLAY ' ' WS-LN-MONTH-CTR
WITH NO ADVANCING
MOVE WS-LN-PAYMENT TO WS-FMT-AMOUNT
DISPLAY ' ' WS-FMT-AMOUNT WITH NO ADVANCING
MOVE WS-LN-INTEREST-PORTION TO WS-FMT-AMOUNT
DISPLAY ' ' WS-FMT-AMOUNT WITH NO ADVANCING
MOVE WS-LN-PRINCIPAL-PORTION TO WS-FMT-AMOUNT
DISPLAY ' ' WS-FMT-AMOUNT WITH NO ADVANCING
MOVE WS-LN-REMAINING-BAL TO WS-FMT-AMOUNT
DISPLAY ' ' WS-FMT-AMOUNT.
Solution Walkthrough
The Power of INTEGER-OF-DATE / DATE-OF-INTEGER
The entire business day calculation rests on a simple pattern: convert a date to an integer, add 1, check if the result is a business day, and repeat until the required number of business days have been counted. The intrinsic functions handle all calendar complexity internally. The code never needs to check whether February has 28 or 29 days, whether April has 30 days, or what happens at year boundaries. All of that logic is embedded in the compiler's implementation of INTEGER-OF-DATE and DATE-OF-INTEGER.
Compare this to the legacy DATECALC subroutine, which contained an explicit leap year check:
* Legacy approach (replaced):
IF WS-YEAR / 4 = FUNCTION INTEGER-PART(WS-YEAR / 4)
AND (WS-YEAR / 100
NOT = FUNCTION INTEGER-PART(WS-YEAR / 100)
OR WS-YEAR / 400
= FUNCTION INTEGER-PART(WS-YEAR / 400))
MOVE 29 TO WS-FEB-DAYS
ELSE
MOVE 28 TO WS-FEB-DAYS
END-IF
The intrinsic function approach eliminates this entirely. The conversion functions know the calendar rules, and the programmer never needs to think about them.
FUNCTION MOD for Day-of-Week
Determining the day of the week is a single computation: FUNCTION MOD(integer-date, 7). The result maps to a specific day depending on the epoch. The programmer verifies the mapping once with a known date (e.g., February 10, 2026 is a Tuesday) and then uses the mapping table throughout the application. This replaces the traditional Zeller's congruence algorithm that the legacy subroutine implemented in twenty-seven lines of code.
FUNCTION ANNUITY for Loan Payments
The monthly payment calculation is perhaps the most dramatic simplification. The traditional formula requires:
Payment = Principal * (r * (1+r)^n) / ((1+r)^n - 1)
where r is the monthly rate and n is the number of months. In COBOL, implementing this formula manually requires careful handling of the exponentiation and potential overflow. With FUNCTION ANNUITY, the calculation is a single COMPUTE statement that the compiler optimizes internally.
FUNCTION TEST-DATE-YYYYMMDD for Validation
The aging report demonstrates defensive programming: before computing the days between two dates, the code validates each date with FUNCTION TEST-DATE-YYYYMMDD. This prevents runtime exceptions from corrupt or missing date values -- a common problem when processing data from external systems or legacy files.
Results and Impact
The replacement utility reduced date calculation code from 600 lines to approximately 200 lines -- a 67% reduction. More importantly:
- Bug count dropped to zero in the first six months of production use. The legacy subroutine had averaged three date-related bug reports per quarter.
- New calculations were added in hours instead of days. The aging report, which would have required weeks of custom coding with the legacy subroutine, was implemented in a single afternoon.
- Testing became straightforward. Each intrinsic function has well-defined behavior specified in the COBOL standard. The team only needed to test their business logic, not the calendar arithmetic.
Discussion Questions
-
The business day calculation loops through each day individually, checking whether it is a weekend or holiday. For large values (e.g., 250 business days), this could be slow. How could you optimize the algorithm to reduce the number of iterations while still handling holidays correctly?
-
The 30/360 day-count convention treats every month as 30 days, which simplifies calculations but introduces approximation. What financial instruments would require actual/actual day counting instead? How would you implement actual/actual using INTEGER-OF-DATE?
-
The holiday table is hardcoded in WORKING-STORAGE. In production, holidays vary by country, by state, and by financial market. Design a data structure and loading strategy for a multi-market holiday calendar that supports US, UK, and EU holidays simultaneously.
-
The loan amortization uses
FUNCTION ANNUITYfor fixed-rate calculations. How would you modify the program to handle an adjustable-rate mortgage (ARM) where the rate changes every 12 months? Which intrinsic functions would still be useful, and which calculations would need custom code? -
The
FUNCTION CURRENT-DATEcall at the beginning of the program captures the date once and uses it throughout. Why is this better than calling CURRENT-DATE each time a timestamp is needed? In what scenarios might you need to call CURRENT-DATE multiple times within a single program run?