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:

  1. 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.

  2. 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.

  3. 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.

  4. Maturity Date Schedule -- Given an origination date and a term in months, generate a schedule of monthly maturity dates, adjusting for month-end conventions.

  5. 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.

  6. 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

  1. 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?

  2. 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?

  3. 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.

  4. The loan amortization uses FUNCTION ANNUITY for 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?

  5. The FUNCTION CURRENT-DATE call 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?