20 min read

> "If you want to understand why COBOL developers are careful, methodical, and slightly paranoid about testing, ask them about Y2K. We rewrote the world's date processing in two years — and it worked." — James Okafor, Team Lead, MedClaim

Chapter 21: Date and Time Processing

"If you want to understand why COBOL developers are careful, methodical, and slightly paranoid about testing, ask them about Y2K. We rewrote the world's date processing in two years — and it worked." — James Okafor, Team Lead, MedClaim

Date and time processing is one of the most error-prone areas in all of programming. Leap years, month-end boundaries, timezone conversions, daylight saving time transitions, fiscal year calculations, business day exclusions — every one of these has been the root cause of production failures in financial and healthcare systems. And underlying all of these is the defining event in COBOL's modern history: the Y2K crisis.

In this chapter, we build a comprehensive understanding of date processing in COBOL. We start with the historical context of Y2K and the lessons it taught. We then cover COBOL's date formats, the intrinsic functions for date arithmetic, Language Environment date services, validation techniques, business day calculations, and timezone handling. Throughout, we apply these concepts to realistic problems at GlobalBank (transaction dates, statement periods, aging reports) and MedClaim (filing deadlines, claim aging, timely filing validation).

21.1 The Y2K Problem: Lessons Learned

Historical Context

From its creation in 1959 until the late 1990s, the vast majority of COBOL programs stored years as two digits. A date like January 15, 1985 was stored as 850115. This was not laziness — it was practical: storage was extraordinarily expensive in the 1960s and 1970s, and two bytes per date field across millions of records added up. No one designing systems in 1965 imagined they would still be running in 2000.

As the year 2000 approached, the problem became clear: a 2-digit year of "00" would be interpreted as 1900, not 2000. Date comparisons would fail (is "00" greater than or less than "99"?), date arithmetic would produce negative results, and sorting by date would produce reversed sequences.

The Scale of Y2K

The Y2K remediation effort was staggering: - Estimated global cost: $300-600 billion - Lines of COBOL code analyzed: Over 200 billion - COBOL programmers recruited: Thousands of retired programmers returned to the workforce - Timeline: Most organizations began in 1996-1997 and worked through December 31, 1999

Two Approaches: Windowing vs. Expansion

Two primary strategies emerged:

Date windowing preserved the 2-digit year but applied a "pivot year" rule: if the year is >= 50, it belongs to the 1900s; if < 50, it belongs to the 2000s. This was faster to implement but created a new expiration date (2050).

*> Windowing approach
IF WS-2DIGIT-YEAR >= 50
    MOVE 19 TO WS-CENTURY
ELSE
    MOVE 20 TO WS-CENTURY
END-IF
STRING WS-CENTURY WS-2DIGIT-YEAR
    DELIMITED BY SIZE INTO WS-4DIGIT-YEAR

Date expansion converted all date fields from 2 digits to 4 digits. This was the permanent fix but required changing file layouts, database schemas, copybooks, program logic, and every interface — a massive undertaking.

*> Before Y2K:
01  WS-DATE  PIC 9(6).    *> YYMMDD

*> After Y2K expansion:
01  WS-DATE  PIC 9(8).    *> YYYYMMDD

🔴 Critical Lesson: Date windowing is still in production at many organizations. Some systems use pivot years of 2040 or 2050. If you encounter windowing code, it has an expiration date. Plan for expansion before the window closes.

Legacy of Y2K

The Y2K crisis left several lasting impacts on COBOL development:

  1. 4-digit years are now standard. Every new COBOL program uses YYYYMMDD.
  2. Date validation became mandatory. Never trust a date without validating it.
  3. Testing discipline improved. Y2K forced rigorous testing practices.
  4. COBOL's importance was confirmed. The crisis proved that COBOL systems are essential infrastructure.
  5. Documentation matters. Many Y2K bugs were found in undocumented date assumptions.

21.2 Date Formats in COBOL

COBOL programs encounter several date formats. Understanding each is essential.

Gregorian Date: YYYYMMDD

The standard format for modern COBOL programs:

01  WS-GREG-DATE.
    05  WS-GD-YEAR    PIC 9(4).
    05  WS-GD-MONTH   PIC 9(2).
    05  WS-GD-DAY     PIC 9(2).

Julian Date: YYYYDDD

The year followed by the day-of-year (1-366):

01  WS-JULIAN-DATE.
    05  WS-JD-YEAR    PIC 9(4).
    05  WS-JD-DAY     PIC 9(3).

Julian dates are used in some mainframe scheduling systems and file naming conventions.

Packed Date

Dates stored in COMP-3 (packed decimal) format to save storage:

01  WS-PACKED-DATE    PIC 9(8) COMP-3.
    *> Occupies 5 bytes instead of 8

Integer Date (Lilian Date)

The number of days since a base date. COBOL's intrinsic functions use the Gregorian calendar epoch (day 1 = January 1, 1601):

01  WS-INT-DATE   PIC 9(7).
    COMPUTE WS-INT-DATE =
        FUNCTION INTEGER-OF-DATE(20240615)
    *> Returns a large integer representing June 15, 2024

IBM Lilian Date

IBM's Language Environment uses October 15, 1582 (the start of the Gregorian calendar) as day 1. This differs from the COBOL intrinsic function epoch. Be aware of which system you are using.

CICS ABSTIME

CICS uses a packed decimal timestamp representing the number of milliseconds since January 1, 1900:

01  WS-ABSTIME   PIC S9(15) COMP-3.

Timestamps and Date-Time Formats

Full timestamps include both date and time:

01  WS-TIMESTAMP.
    05  WS-TS-DATE    PIC 9(8).     *> YYYYMMDD
    05  WS-TS-TIME    PIC 9(6).     *> HHMMSS
    05  WS-TS-FRAC    PIC 9(6).     *> Microseconds

DB2 timestamps use the format YYYY-MM-DD-HH.MM.SS.FFFFFF.

21.3 FUNCTION CURRENT-DATE

The CURRENT-DATE function is the primary way to obtain the current date and time in COBOL. It returns a 21-character alphanumeric string:

01  WS-CURRENT-DT.
    05  WS-CD-DATE.
        10  WS-CD-YEAR      PIC 9(4).   *> Pos 1-4
        10  WS-CD-MONTH     PIC 9(2).   *> Pos 5-6
        10  WS-CD-DAY       PIC 9(2).   *> Pos 7-8
    05  WS-CD-TIME.
        10  WS-CD-HOURS     PIC 9(2).   *> Pos 9-10
        10  WS-CD-MINUTES   PIC 9(2).   *> Pos 11-12
        10  WS-CD-SECONDS   PIC 9(2).   *> Pos 13-14
        10  WS-CD-HUNDREDTHS PIC 9(2).  *> Pos 15-16
    05  WS-CD-GMT-OFFSET.
        10  WS-CD-GMT-SIGN  PIC X(1).   *> Pos 17
        10  WS-CD-GMT-HOURS PIC 9(2).   *> Pos 18-19
        10  WS-CD-GMT-MINS  PIC 9(2).   *> Pos 20-21

PROCEDURE DIVISION.
    MOVE FUNCTION CURRENT-DATE TO WS-CURRENT-DT

    DISPLAY "TODAY: " WS-CD-YEAR "-"
            WS-CD-MONTH "-" WS-CD-DAY
    DISPLAY "TIME:  " WS-CD-HOURS ":"
            WS-CD-MINUTES ":" WS-CD-SECONDS
    DISPLAY "GMT OFFSET: " WS-CD-GMT-SIGN
            WS-CD-GMT-HOURS ":" WS-CD-GMT-MINS

Positions Explained

Positions Content Example
1-4 Year (4 digits) 2024
5-6 Month (01-12) 06
7-8 Day (01-31) 15
9-10 Hours (00-23) 14
11-12 Minutes (00-59) 30
13-14 Seconds (00-59) 45
15-16 Hundredths of seconds 12
17 GMT sign (+/-) -
18-19 GMT offset hours 05
20-21 GMT offset minutes 00

A value of 202406151430451200500 means June 15, 2024 at 14:30:45.12, 5 hours behind GMT (US Eastern Daylight Time).

Using CURRENT-DATE for Timing

01  WS-START-TIME  PIC X(21).
01  WS-END-TIME    PIC X(21).
01  WS-ELAPSED     PIC 9(7).

    MOVE FUNCTION CURRENT-DATE TO WS-START-TIME

    PERFORM 3000-PROCESS-BATCH

    MOVE FUNCTION CURRENT-DATE TO WS-END-TIME

    *> Compute elapsed seconds (simplified)
    COMPUTE WS-ELAPSED =
        (FUNCTION NUMVAL(WS-END-TIME(9:2)) * 3600
         + FUNCTION NUMVAL(WS-END-TIME(11:2)) * 60
         + FUNCTION NUMVAL(WS-END-TIME(13:2)))
      - (FUNCTION NUMVAL(WS-START-TIME(9:2)) * 3600
         + FUNCTION NUMVAL(WS-START-TIME(11:2)) * 60
         + FUNCTION NUMVAL(WS-START-TIME(13:2)))

    DISPLAY "ELAPSED: " WS-ELAPSED " SECONDS"

21.4 Date Arithmetic with Intrinsic Functions

COBOL's date arithmetic relies on converting calendar dates to integer day numbers, performing arithmetic on the integers, and converting back.

INTEGER-OF-DATE: Calendar to Integer

01  WS-DATE-1    PIC 9(8) VALUE 20240101.
01  WS-INT-1     PIC 9(7).

    COMPUTE WS-INT-1 =
        FUNCTION INTEGER-OF-DATE(WS-DATE-1)

The function accepts a date in YYYYMMDD format and returns an integer. Day 1 in the COBOL system is January 1, 1601.

DATE-OF-INTEGER: Integer to Calendar

01  WS-NEW-DATE  PIC 9(8).

    COMPUTE WS-NEW-DATE =
        FUNCTION DATE-OF-INTEGER(WS-INT-1 + 90)
    *> Adds 90 days to the original date

Days Between Two Dates

01  WS-DATE-FROM  PIC 9(8) VALUE 20240101.
01  WS-DATE-TO    PIC 9(8) VALUE 20240615.
01  WS-DAYS-DIFF  PIC S9(5).

    COMPUTE WS-DAYS-DIFF =
        FUNCTION INTEGER-OF-DATE(WS-DATE-TO)
      - FUNCTION INTEGER-OF-DATE(WS-DATE-FROM)

    DISPLAY "DAYS BETWEEN: " WS-DAYS-DIFF
    *> DAYS BETWEEN: 166

Adding/Subtracting Days

    *> Add 30 days
    COMPUTE WS-FUTURE-DATE =
        FUNCTION DATE-OF-INTEGER(
            FUNCTION INTEGER-OF-DATE(WS-START-DATE)
            + 30)

    *> Subtract 7 days
    COMPUTE WS-WEEK-AGO =
        FUNCTION DATE-OF-INTEGER(
            FUNCTION INTEGER-OF-DATE(WS-TODAY)
            - 7)

Julian Date Functions

For Julian dates (YYYYDDD), use INTEGER-OF-DAY and DAY-OF-INTEGER:

    COMPUTE WS-JULIAN-INT =
        FUNCTION INTEGER-OF-DAY(2024166)
    *> Day 166 of 2024

    COMPUTE WS-JULIAN-DATE =
        FUNCTION DAY-OF-INTEGER(WS-JULIAN-INT)

Converting Between Gregorian and Julian

    *> Gregorian to Julian
    COMPUTE WS-JULIAN =
        FUNCTION DAY-OF-INTEGER(
            FUNCTION INTEGER-OF-DATE(WS-GREG-DATE))

    *> Julian to Gregorian
    COMPUTE WS-GREG =
        FUNCTION DATE-OF-INTEGER(
            FUNCTION INTEGER-OF-DAY(WS-JULIAN-DATE))

21.5 Date Validation Techniques

Invalid dates are a constant source of production problems. COBOL does not automatically validate dates — you must do it explicitly.

Basic Validation with Intrinsic Functions

The simplest validation approach exploits the fact that INTEGER-OF-DATE causes a runtime error for invalid dates. By wrapping it in a defensive check:

       01  WS-VALID-FLAG  PIC X(1).
           88  WS-DATE-VALID    VALUE "Y".
           88  WS-DATE-INVALID  VALUE "N".

       VALIDATE-DATE.
           SET WS-DATE-VALID TO TRUE

      *    Check numeric
           IF WS-INPUT-DATE IS NOT NUMERIC
               SET WS-DATE-INVALID TO TRUE
               EXIT PARAGRAPH
           END-IF

      *    Check year range
           IF WS-INPUT-DATE(1:4) < "1900"
           OR WS-INPUT-DATE(1:4) > "2099"
               SET WS-DATE-INVALID TO TRUE
               EXIT PARAGRAPH
           END-IF

      *    Check month range
           IF WS-INPUT-DATE(5:2) < "01"
           OR WS-INPUT-DATE(5:2) > "12"
               SET WS-DATE-INVALID TO TRUE
               EXIT PARAGRAPH
           END-IF

      *    Check day range (basic)
           IF WS-INPUT-DATE(7:2) < "01"
           OR WS-INPUT-DATE(7:2) > "31"
               SET WS-DATE-INVALID TO TRUE
               EXIT PARAGRAPH
           END-IF

      *    Check month/day validity using a table
           PERFORM VALIDATE-MONTH-DAY
           .

Comprehensive Month-Day Validation

       01  DAYS-IN-MONTH-TABLE.
           05  FILLER  PIC 99 VALUE 31.  *> Jan
           05  FILLER  PIC 99 VALUE 28.  *> Feb (non-leap)
           05  FILLER  PIC 99 VALUE 31.  *> Mar
           05  FILLER  PIC 99 VALUE 30.  *> Apr
           05  FILLER  PIC 99 VALUE 31.  *> May
           05  FILLER  PIC 99 VALUE 30.  *> Jun
           05  FILLER  PIC 99 VALUE 31.  *> Jul
           05  FILLER  PIC 99 VALUE 31.  *> Aug
           05  FILLER  PIC 99 VALUE 30.  *> Sep
           05  FILLER  PIC 99 VALUE 31.  *> Oct
           05  FILLER  PIC 99 VALUE 30.  *> Nov
           05  FILLER  PIC 99 VALUE 31.  *> Dec
       01  DAYS-TABLE REDEFINES DAYS-IN-MONTH-TABLE.
           05  MAX-DAYS  PIC 99 OCCURS 12 TIMES.

       01  WS-INPUT-YEAR   PIC 9(4).
       01  WS-INPUT-MONTH  PIC 9(2).
       01  WS-INPUT-DAY    PIC 9(2).
       01  WS-MAX-DAY      PIC 99.

       VALIDATE-MONTH-DAY.
           MOVE WS-INPUT-DATE(1:4) TO WS-INPUT-YEAR
           MOVE WS-INPUT-DATE(5:2) TO WS-INPUT-MONTH
           MOVE WS-INPUT-DATE(7:2) TO WS-INPUT-DAY

           MOVE MAX-DAYS(WS-INPUT-MONTH) TO WS-MAX-DAY

      *    Adjust February for leap year
           IF WS-INPUT-MONTH = 2
               PERFORM CHECK-LEAP-YEAR
               IF WS-IS-LEAP-YEAR
                   MOVE 29 TO WS-MAX-DAY
               END-IF
           END-IF

           IF WS-INPUT-DAY > WS-MAX-DAY
               SET WS-DATE-INVALID TO TRUE
           END-IF
           .

       CHECK-LEAP-YEAR.
           SET WS-NOT-LEAP-YEAR TO TRUE
           IF FUNCTION MOD(WS-INPUT-YEAR, 4) = ZERO
               SET WS-IS-LEAP-YEAR TO TRUE
               IF FUNCTION MOD(WS-INPUT-YEAR, 100)
                  = ZERO
                   SET WS-NOT-LEAP-YEAR TO TRUE
                   IF FUNCTION MOD(WS-INPUT-YEAR, 400)
                      = ZERO
                       SET WS-IS-LEAP-YEAR TO TRUE
                   END-IF
               END-IF
           END-IF
           .

⚠️ The Leap Year Rule: A year is a leap year if it is divisible by 4, EXCEPT if it is divisible by 100, EXCEPT if it is also divisible by 400. Thus: 2024 is a leap year (divisible by 4), 1900 was NOT a leap year (divisible by 100 but not 400), and 2000 WAS a leap year (divisible by 400). Getting this wrong is the most common date validation bug.

TEST-DATE-YYYYMMDD (IBM Extension)

IBM Enterprise COBOL provides the FUNCTION TEST-DATE-YYYYMMDD intrinsic function:

    IF FUNCTION TEST-DATE-YYYYMMDD(WS-INPUT-DATE)
       = ZERO
        SET WS-DATE-VALID TO TRUE
    ELSE
        SET WS-DATE-INVALID TO TRUE
    END-IF

This handles all validation including leap year checking in a single call. However, it is an IBM extension not available on all compilers.

21.6 Language Environment Date Services

On IBM z/OS systems, the Language Environment (LE) provides callable date services that offer additional capabilities beyond the COBOL intrinsic functions.

CEECBLDY — Convert Date to Lilian

    CALL "CEECBLDY" USING
        WS-DATE-STRING
        WS-DATE-FORMAT
        WS-LILIAN-DATE
        WS-FEEDBACK-CODE

CEEDATM — Convert Lilian to Formatted Date

    CALL "CEEDATM" USING
        WS-LILIAN-DATE
        WS-OUTPUT-FORMAT
        WS-FORMATTED-DATE
        WS-FEEDBACK-CODE

Common LE Date Format Strings

Format Meaning Example
YYYYMMDD Standard numeric 20240615
YYYY-MM-DD ISO with dashes 2024-06-15
MM/DD/YYYY US format 06/15/2024
DD-MMM-YYYY International 15-JUN-2024
YYYYDDD Julian 2024167

💡 When to Use LE Date Services: Use intrinsic functions for basic date arithmetic (they are faster and more portable). Use LE date services when you need formatted output (e.g., "June 15, 2024"), when you need to parse dates in various input formats, or when interfacing with other LE-compatible languages.

21.7 Date Aging and Difference Calculations

Aging calculations determine how old something is — a claim, an account receivable, a pending transaction. They are fundamental to financial and healthcare systems.

Simple Aging

       COMPUTE-AGE-IN-DAYS.
           MOVE FUNCTION CURRENT-DATE(1:8)
               TO WS-TODAY
           COMPUTE WS-AGE-DAYS =
               FUNCTION INTEGER-OF-DATE(WS-TODAY)
             - FUNCTION INTEGER-OF-DATE(WS-ITEM-DATE)
           .

Aging Buckets

Both GlobalBank and MedClaim use aging buckets to categorize overdue items:

       01  WS-AGING-BUCKETS.
           05  WS-BUCKET-CURRENT  PIC 9(9)V99.
           05  WS-BUCKET-30       PIC 9(9)V99.
           05  WS-BUCKET-60       PIC 9(9)V99.
           05  WS-BUCKET-90       PIC 9(9)V99.
           05  WS-BUCKET-OVER-90  PIC 9(9)V99.
       01  WS-AGING-COUNTS.
           05  WS-COUNT-CURRENT   PIC 9(7).
           05  WS-COUNT-30        PIC 9(7).
           05  WS-COUNT-60        PIC 9(7).
           05  WS-COUNT-90        PIC 9(7).
           05  WS-COUNT-OVER-90   PIC 9(7).

       ASSIGN-AGING-BUCKET.
           EVALUATE TRUE
               WHEN WS-AGE-DAYS <= 30
                   ADD WS-ITEM-AMOUNT
                       TO WS-BUCKET-CURRENT
                   ADD 1 TO WS-COUNT-CURRENT
               WHEN WS-AGE-DAYS <= 60
                   ADD WS-ITEM-AMOUNT
                       TO WS-BUCKET-30
                   ADD 1 TO WS-COUNT-30
               WHEN WS-AGE-DAYS <= 90
                   ADD WS-ITEM-AMOUNT
                       TO WS-BUCKET-60
                   ADD 1 TO WS-COUNT-60
               WHEN WS-AGE-DAYS <= 120
                   ADD WS-ITEM-AMOUNT
                       TO WS-BUCKET-90
                   ADD 1 TO WS-COUNT-90
               WHEN OTHER
                   ADD WS-ITEM-AMOUNT
                       TO WS-BUCKET-OVER-90
                   ADD 1 TO WS-COUNT-OVER-90
           END-EVALUATE
           .

Month-Based Aging

Some aging calculations require month differences rather than day counts:

       COMPUTE-MONTH-AGE.
           MOVE WS-TODAY(1:4) TO WS-TODAY-YEAR
           MOVE WS-TODAY(5:2) TO WS-TODAY-MONTH
           MOVE WS-ITEM-DATE(1:4) TO WS-ITEM-YEAR
           MOVE WS-ITEM-DATE(5:2) TO WS-ITEM-MONTH

           COMPUTE WS-MONTH-AGE =
               (WS-TODAY-YEAR - WS-ITEM-YEAR) * 12
             + (WS-TODAY-MONTH - WS-ITEM-MONTH)
           .

21.8 End-of-Month and Month-End Processing

Month-end processing is one of the most critical date operations in financial systems. Determining the last day of a month, the first day of the next month, and handling the transition between months correctly affects everything from interest calculations to regulatory reporting.

Computing the Last Day of Any Month

The most reliable way to find the last day of a month is to compute the first day of the next month and subtract one day:

       01  WS-LAST-DAY-RESULT  PIC 9(8).
       01  WS-FIRST-NEXT       PIC 9(8).
       01  WS-WORK-YEAR        PIC 9(4).
       01  WS-WORK-MONTH       PIC 9(2).

       COMPUTE-LAST-DAY-OF-MONTH.
      *    Input: WS-WORK-YEAR and WS-WORK-MONTH
      *    Output: WS-LAST-DAY-RESULT

      *    Build first day of next month
           IF WS-WORK-MONTH = 12
               COMPUTE WS-FIRST-NEXT =
                   (WS-WORK-YEAR + 1) * 10000 + 0101
           ELSE
               COMPUTE WS-FIRST-NEXT =
                   WS-WORK-YEAR * 10000
                   + (WS-WORK-MONTH + 1) * 100 + 01
           END-IF

      *    Subtract one day
           COMPUTE WS-LAST-DAY-RESULT =
               FUNCTION DATE-OF-INTEGER(
                   FUNCTION INTEGER-OF-DATE(
                       WS-FIRST-NEXT) - 1)
           .

This approach is elegant because it works for all months and all years without any lookup tables or leap year checks — the intrinsic functions handle everything.

End-of-Month Interest Calculation

GlobalBank calculates daily interest and posts it on the last day of each month. The calculation must use the actual number of days in the month for the daily rate:

       COMPUTE-MONTHLY-INTEREST.
      *    Determine days in current month
           MOVE WS-ACCT-YEAR TO WS-WORK-YEAR
           MOVE WS-ACCT-MONTH TO WS-WORK-MONTH
           PERFORM COMPUTE-LAST-DAY-OF-MONTH

           COMPUTE WS-DAYS-IN-MONTH =
               WS-LAST-DAY-RESULT -
               (WS-WORK-YEAR * 10000
                + WS-WORK-MONTH * 100) + 1
      *    Hmm, that's tricky. Better approach:
           COMPUTE WS-FIRST-OF-MONTH =
               WS-WORK-YEAR * 10000
               + WS-WORK-MONTH * 100 + 01
           COMPUTE WS-DAYS-IN-MONTH =
               FUNCTION INTEGER-OF-DATE(
                   WS-LAST-DAY-RESULT)
             - FUNCTION INTEGER-OF-DATE(
                   WS-FIRST-OF-MONTH) + 1

      *    Daily interest = balance * annual rate / 365
           COMPUTE WS-DAILY-RATE =
               WS-ANNUAL-RATE / 365

      *    Monthly interest = balance * daily rate * days
           COMPUTE WS-MONTHLY-INTEREST =
               WS-ACCT-BALANCE * WS-DAILY-RATE
               * WS-DAYS-IN-MONTH

           DISPLAY "MONTH: " WS-WORK-MONTH
                   " DAYS: " WS-DAYS-IN-MONTH
                   " INTEREST: " WS-MONTHLY-INTEREST
           .

💡 Actual/365 vs. 30/360: The calculation above uses the Actual/365 day count convention — actual days in the month divided by 365 days in the year. Some financial products use the 30/360 convention where every month is assumed to have 30 days and the year has 360 days. The convention used must match the product's legal documentation. GlobalBank uses Actual/365 for savings accounts and 30/360 for mortgages.

Month-End Batch Scheduling

At GlobalBank, different processes run on different month-end dates:

       01  WS-ME-DATES.
           05  WS-ME-LAST-BUS-DAY   PIC 9(8).
           05  WS-ME-LAST-CAL-DAY   PIC 9(8).
           05  WS-ME-FIRST-NEXT     PIC 9(8).

       COMPUTE-MONTH-END-DATES.
      *    Last calendar day of month
           PERFORM COMPUTE-LAST-DAY-OF-MONTH
           MOVE WS-LAST-DAY-RESULT TO WS-ME-LAST-CAL-DAY

      *    Last business day of month
      *    (back up from last calendar day until
      *     we find a business day)
           MOVE WS-ME-LAST-CAL-DAY TO WS-ME-LAST-BUS-DAY
           MOVE WS-ME-LAST-BUS-DAY TO WS-INPUT-DATE
           PERFORM IS-WEEKEND
           PERFORM IS-HOLIDAY
           PERFORM UNTIL WS-IS-BUSINESS-DAY
                     AND WS-NOT-HOLIDAY
               COMPUTE WS-ME-LAST-BUS-DAY =
                   FUNCTION DATE-OF-INTEGER(
                       FUNCTION INTEGER-OF-DATE(
                           WS-ME-LAST-BUS-DAY) - 1)
               MOVE WS-ME-LAST-BUS-DAY TO WS-INPUT-DATE
               PERFORM IS-WEEKEND
               PERFORM IS-HOLIDAY
           END-PERFORM

      *    First day of next month
           IF WS-WORK-MONTH = 12
               COMPUTE WS-ME-FIRST-NEXT =
                   (WS-WORK-YEAR + 1) * 10000 + 0101
           ELSE
               COMPUTE WS-ME-FIRST-NEXT =
                   WS-WORK-YEAR * 10000
                   + (WS-WORK-MONTH + 1) * 100 + 01
           END-IF

           DISPLAY "LAST CAL DAY:  " WS-ME-LAST-CAL-DAY
           DISPLAY "LAST BUS DAY:  " WS-ME-LAST-BUS-DAY
           DISPLAY "FIRST NEXT MO: " WS-ME-FIRST-NEXT
           .

⚖️ Theme: Defensive Programming. Month-end processing is when most date-related production failures occur. The last day of February, the transition from December to January, and months where the last day falls on a weekend are all high-risk scenarios. Maria Chen's team runs a "month-end rehearsal" on the 25th of each month, processing the full month-end batch against a copy of production data to catch any date-related issues before the actual month-end.

Period-Over-Period Comparison Dates

For reporting, you often need "same day last month" or "same day last year." These are trickier than they seem because months have different lengths:

       COMPUTE-SAME-DAY-LAST-MONTH.
      *    Input: WS-REFERENCE-DATE (YYYYMMDD)
      *    Output: WS-LAST-MONTH-DATE

           MOVE WS-REFERENCE-DATE(1:4)
               TO WS-WORK-YEAR
           MOVE WS-REFERENCE-DATE(5:2)
               TO WS-WORK-MONTH
           MOVE WS-REFERENCE-DATE(7:2)
               TO WS-WORK-DAY

      *    Go back one month
           IF WS-WORK-MONTH = 1
               MOVE 12 TO WS-WORK-MONTH
               SUBTRACT 1 FROM WS-WORK-YEAR
           ELSE
               SUBTRACT 1 FROM WS-WORK-MONTH
           END-IF

      *    Adjust day if it exceeds last day of that month
           PERFORM COMPUTE-LAST-DAY-OF-MONTH
           MOVE WS-LAST-DAY-RESULT(7:2)
               TO WS-MAX-DAY

           IF WS-WORK-DAY > WS-MAX-DAY
               MOVE WS-MAX-DAY TO WS-WORK-DAY
           END-IF

           COMPUTE WS-LAST-MONTH-DATE =
               WS-WORK-YEAR * 10000
               + WS-WORK-MONTH * 100
               + WS-WORK-DAY
           .

This correctly handles the case where the reference date is March 31 and "same day last month" would be February 31 (which does not exist) — it adjusts to February 28 (or 29 in a leap year).

21.9 Timezone Handling

In a global banking system, transactions occur across time zones. COBOL's CURRENT-DATE provides the GMT offset, enabling timezone calculations.

Extracting the GMT Offset

    MOVE FUNCTION CURRENT-DATE TO WS-CURRENT-DT

    *> WS-CD-GMT-SIGN is "+" or "-"
    *> WS-CD-GMT-HOURS is the hour offset
    *> WS-CD-GMT-MINS is the minute offset

    EVALUATE WS-CD-GMT-SIGN
        WHEN "-"
            COMPUTE WS-GMT-OFFSET-MINS =
                (WS-CD-GMT-HOURS * 60
                 + WS-CD-GMT-MINS) * -1
        WHEN "+"
            COMPUTE WS-GMT-OFFSET-MINS =
                WS-CD-GMT-HOURS * 60
                + WS-CD-GMT-MINS
    END-EVALUATE

Converting Local Time to GMT

       CONVERT-TO-GMT.
      *    Get local time
           MOVE FUNCTION CURRENT-DATE TO WS-CURRENT-DT

      *    Convert to minutes since midnight
           COMPUTE WS-LOCAL-MINS =
               WS-CD-HOURS * 60 + WS-CD-MINUTES

      *    Subtract GMT offset to get GMT
           COMPUTE WS-GMT-MINS =
               WS-LOCAL-MINS - WS-GMT-OFFSET-MINS

      *    Handle day boundary
           IF WS-GMT-MINS < 0
               ADD 1440 TO WS-GMT-MINS
               *> Subtract 1 day from date
               COMPUTE WS-GMT-DATE =
                   FUNCTION DATE-OF-INTEGER(
                       FUNCTION INTEGER-OF-DATE(
                           WS-TODAY) - 1)
           ELSE IF WS-GMT-MINS >= 1440
               SUBTRACT 1440 FROM WS-GMT-MINS
               *> Add 1 day to date
               COMPUTE WS-GMT-DATE =
                   FUNCTION DATE-OF-INTEGER(
                       FUNCTION INTEGER-OF-DATE(
                           WS-TODAY) + 1)
           ELSE
               MOVE WS-TODAY TO WS-GMT-DATE
           END-IF

           COMPUTE WS-GMT-HOURS =
               FUNCTION INTEGER-PART(
                   WS-GMT-MINS / 60)
           COMPUTE WS-GMT-MINUTES =
               FUNCTION MOD(WS-GMT-MINS, 60)
           .

⚖️ The Modernization Spectrum: Timezone handling is a perfect example of how COBOL programs must interact with modern global systems. While COBOL's CURRENT-DATE provides the raw offset, production systems often interface with timezone databases (the IANA/Olson database) through web service calls or shared data areas. The COBOL program provides the business logic; the timezone infrastructure provides the geographic intelligence.

21.9 Business Day Calculations

Financial systems frequently need to determine business days — excluding weekends and holidays.

Day-of-Week Calculation

Using INTEGER-OF-DATE and MOD to determine the day of the week:

       01  WS-DOW         PIC 9.
           88  WS-IS-MONDAY     VALUE 1.
           88  WS-IS-TUESDAY    VALUE 2.
           88  WS-IS-WEDNESDAY  VALUE 3.
           88  WS-IS-THURSDAY   VALUE 4.
           88  WS-IS-FRIDAY     VALUE 5.
           88  WS-IS-SATURDAY   VALUE 6.
           88  WS-IS-SUNDAY     VALUE 7.

       GET-DAY-OF-WEEK.
      *    COBOL integer dates: day 1 (1601-01-01)
      *    was a Monday
           COMPUTE WS-DOW =
               FUNCTION MOD(
                   FUNCTION INTEGER-OF-DATE(WS-DATE)
                   - 1, 7) + 1
           .

💡 Calibration Note: The day-of-week calculation depends on what day of the week corresponds to the epoch. Verify by testing with a known date (e.g., January 1, 2024 was a Monday). Adjust the formula if your compiler's epoch differs.

Checking for Weekends

       IS-WEEKEND.
           PERFORM GET-DAY-OF-WEEK
           IF WS-IS-SATURDAY OR WS-IS-SUNDAY
               SET WS-IS-NON-BUSINESS-DAY TO TRUE
           ELSE
               SET WS-IS-BUSINESS-DAY TO TRUE
           END-IF
           .

Holiday Table

       01  HOLIDAY-TABLE-VALUES.
           05  FILLER PIC 9(8) VALUE 20240101.  *> New Year
           05  FILLER PIC 9(8) VALUE 20240115.  *> MLK Day
           05  FILLER PIC 9(8) VALUE 20240219.  *> Presidents
           05  FILLER PIC 9(8) VALUE 20240527.  *> Memorial
           05  FILLER PIC 9(8) VALUE 20240619.  *> Juneteenth
           05  FILLER PIC 9(8) VALUE 20240704.  *> Independence
           05  FILLER PIC 9(8) VALUE 20240902.  *> Labor Day
           05  FILLER PIC 9(8) VALUE 20241014.  *> Columbus
           05  FILLER PIC 9(8) VALUE 20241111.  *> Veterans
           05  FILLER PIC 9(8) VALUE 20241128.  *> Thanksgiving
           05  FILLER PIC 9(8) VALUE 20241225.  *> Christmas

       01  HOLIDAY-TABLE REDEFINES HOLIDAY-TABLE-VALUES.
           05  HOLIDAY-DATE  PIC 9(8) OCCURS 11 TIMES
                             ASCENDING KEY IS HOLIDAY-DATE
                             INDEXED BY HOL-IDX.

       IS-HOLIDAY.
           SET WS-NOT-HOLIDAY TO TRUE
           SEARCH ALL HOLIDAY-DATE
               AT END
                   SET WS-NOT-HOLIDAY TO TRUE
               WHEN HOLIDAY-DATE(HOL-IDX) = WS-DATE
                   SET WS-IS-HOLIDAY TO TRUE
           END-SEARCH
           .

Adding Business Days

       ADD-BUSINESS-DAYS.
      *    Add WS-BUS-DAYS business days to WS-START-DATE
      *    Result in WS-RESULT-DATE
           MOVE WS-START-DATE TO WS-RESULT-DATE
           MOVE ZERO TO WS-BUS-DAYS-COUNTED

           PERFORM UNTIL WS-BUS-DAYS-COUNTED >=
                         WS-BUS-DAYS-TO-ADD
      *        Advance one calendar day
               COMPUTE WS-RESULT-DATE =
                   FUNCTION DATE-OF-INTEGER(
                       FUNCTION INTEGER-OF-DATE(
                           WS-RESULT-DATE) + 1)

      *        Check if this is a business day
               MOVE WS-RESULT-DATE TO WS-DATE
               PERFORM IS-WEEKEND
               IF WS-IS-BUSINESS-DAY
                   PERFORM IS-HOLIDAY
                   IF WS-NOT-HOLIDAY
                       ADD 1 TO WS-BUS-DAYS-COUNTED
                   END-IF
               END-IF
           END-PERFORM
           .

Quarter and Fiscal Year Calculations

       DETERMINE-QUARTER.
           COMPUTE WS-QUARTER =
               FUNCTION INTEGER(
                   (WS-INPUT-MONTH - 1) / 3) + 1
           .

       DETERMINE-FISCAL-QUARTER.
      *    Fiscal year starts October 1
           IF WS-INPUT-MONTH >= 10
               COMPUTE WS-FISCAL-QUARTER =
                   FUNCTION INTEGER(
                       (WS-INPUT-MONTH - 10) / 3) + 1
               COMPUTE WS-FISCAL-YEAR =
                   WS-INPUT-YEAR + 1
           ELSE
               COMPUTE WS-FISCAL-QUARTER =
                   FUNCTION INTEGER(
                       (WS-INPUT-MONTH + 2) / 3)
               MOVE WS-INPUT-YEAR TO WS-FISCAL-YEAR
           END-IF
           .

21.10 Counting Business Days Between Two Dates

While the previous section showed how to add business days to a date, another common requirement is counting business days between two dates. This is critical for SLA calculations (how many business days has a claim been open?), regulatory reporting (report must be filed within 5 business days), and financial settlement (T+2 settlement counting).

The Algorithm

Counting business days between two dates is straightforward but requires care:

       01  WS-BUS-DAY-COUNT   PIC 9(5).
       01  WS-WALK-DATE       PIC 9(8).
       01  WS-WALK-INT        PIC 9(7).
       01  WS-END-INT         PIC 9(7).
       01  WS-WALK-DOW        PIC 9.

       COUNT-BUSINESS-DAYS.
      *    Count business days from WS-START-DATE
      *    to WS-END-DATE (inclusive of start, exclusive
      *    of end, per financial convention)
           MOVE ZERO TO WS-BUS-DAY-COUNT

           COMPUTE WS-WALK-INT =
               FUNCTION INTEGER-OF-DATE(WS-START-DATE)
           COMPUTE WS-END-INT =
               FUNCTION INTEGER-OF-DATE(WS-END-DATE)

           PERFORM UNTIL WS-WALK-INT >= WS-END-INT
      *        Convert back to calendar date for checks
               COMPUTE WS-WALK-DATE =
                   FUNCTION DATE-OF-INTEGER(WS-WALK-INT)

      *        Check day of week
               COMPUTE WS-WALK-DOW =
                   FUNCTION MOD(WS-WALK-INT - 1, 7) + 1

               IF WS-WALK-DOW NOT = 6
                   AND WS-WALK-DOW NOT = 7
      *            Weekday — check if holiday
                   MOVE WS-WALK-DATE TO WS-DATE
                   PERFORM IS-HOLIDAY
                   IF WS-NOT-HOLIDAY
                       ADD 1 TO WS-BUS-DAY-COUNT
                   END-IF
               END-IF

               ADD 1 TO WS-WALK-INT
           END-PERFORM
           .

💡 Performance Note: For date ranges spanning many years, this day-by-day walk can be slow. An optimized version first calculates complete weeks (each contributing 5 business days), then walks only the remaining partial week. James Okafor's production version at MedClaim uses this optimization:

       COUNT-BUSINESS-DAYS-FAST.
      *    Optimized: calculate whole weeks, walk partial
           COMPUTE WS-TOTAL-DAYS =
               WS-END-INT - WS-WALK-INT
           COMPUTE WS-COMPLETE-WEEKS =
               FUNCTION INTEGER(WS-TOTAL-DAYS / 7)
           COMPUTE WS-BUS-DAY-COUNT =
               WS-COMPLETE-WEEKS * 5
           COMPUTE WS-REMAINING-DAYS =
               WS-TOTAL-DAYS - (WS-COMPLETE-WEEKS * 7)

      *    Walk the remaining days (0-6 iterations max)
           COMPUTE WS-WALK-INT =
               WS-WALK-INT + (WS-COMPLETE-WEEKS * 7)
           PERFORM WS-REMAINING-DAYS TIMES
               COMPUTE WS-WALK-DOW =
                   FUNCTION MOD(WS-WALK-INT - 1, 7) + 1
               IF WS-WALK-DOW NOT = 6
                   AND WS-WALK-DOW NOT = 7
                   ADD 1 TO WS-BUS-DAY-COUNT
               END-IF
               ADD 1 TO WS-WALK-INT
           END-PERFORM

      *    Subtract holidays (requires sorted holiday table)
           PERFORM SUBTRACT-HOLIDAYS-IN-RANGE
           .

SLA Monitoring at MedClaim

MedClaim uses business day counting for regulatory compliance. Insurance companies must respond to clean claims within specific timeframes measured in business days:

       01  WS-SLA-REC.
           05  WS-SLA-CLM-ID       PIC X(12).
           05  WS-SLA-RECV-DATE    PIC 9(8).
           05  WS-SLA-STATUS       PIC X(1).
               88  SLA-PENDING         VALUE "P".
               88  SLA-COMPLETED       VALUE "C".
           05  WS-SLA-BUS-DAYS     PIC 9(3).
           05  WS-SLA-LIMIT        PIC 9(3).
           05  WS-SLA-PCT-USED     PIC 9(3).

       CHECK-CLAIM-SLA.
      *    How many business days since we received this claim?
           MOVE WS-SLA-RECV-DATE TO WS-START-DATE
           MOVE WS-TODAY TO WS-END-DATE
           PERFORM COUNT-BUSINESS-DAYS
           MOVE WS-BUS-DAY-COUNT TO WS-SLA-BUS-DAYS

      *    Calculate percentage of SLA consumed
           IF WS-SLA-LIMIT > ZERO
               COMPUTE WS-SLA-PCT-USED =
                   (WS-SLA-BUS-DAYS * 100) / WS-SLA-LIMIT
           END-IF

      *    Flag claims approaching SLA deadline
           EVALUATE TRUE
               WHEN WS-SLA-PCT-USED >= 100
                   DISPLAY "*** SLA BREACHED *** "
                       WS-SLA-CLM-ID
                       " BUS DAYS: " WS-SLA-BUS-DAYS
                       " LIMIT: " WS-SLA-LIMIT
               WHEN WS-SLA-PCT-USED >= 80
                   DISPLAY "SLA WARNING: "
                       WS-SLA-CLM-ID
                       " AT " WS-SLA-PCT-USED "% OF LIMIT"
               WHEN OTHER
                   CONTINUE
           END-EVALUATE
           .

📊 Metrics: MedClaim's SLA monitoring batch runs nightly, checking approximately 45,000 pending claims. Sarah Kim's team tracks three SLA tiers: urgent (5 business days), standard (15 business days), and complex (30 business days). The business day calculation accuracy is critical — an off-by-one error in 2019 caused 200 claims to be reported as SLA-compliant when they were actually one day over, resulting in $47,000 in regulatory penalties.

T+2 Settlement Date Calculation

Securities transactions settle on a T+2 basis (trade date plus two business days). GlobalBank's settlement system uses this calculation daily:

       CALC-SETTLEMENT-DATE.
      *    T+2: Settlement is 2 business days after trade
           MOVE WS-TRADE-DATE TO WS-START-DATE
           MOVE 2 TO WS-BUS-DAYS-TO-ADD
           PERFORM ADD-BUSINESS-DAYS
           MOVE WS-RESULT-DATE TO WS-SETTLE-DATE

      *    Display for confirmation
           DISPLAY "TRADE DATE:      " WS-TRADE-DATE
           DISPLAY "SETTLEMENT DATE: " WS-SETTLE-DATE
           .

When the trade date is a Friday, T+2 settlement falls on Tuesday. When Monday is a holiday, T+2 from Friday becomes Wednesday. These seemingly simple rules require the full business day calculation machinery.

🔗 Cross-Reference: The holiday table used in business day calculations is a table handling problem (Chapter 18). In production, the holiday table is loaded from a file or database at program initialization, allowing it to be updated without recompiling the program.

Floating Holidays

The holiday table above uses fixed dates. But some holidays float — Thanksgiving is the fourth Thursday in November, Memorial Day is the last Monday in May, and Easter follows a complex lunar calendar. Computing floating holidays requires additional logic:

       COMPUTE-THANKSGIVING.
      *    Fourth Thursday in November for given year
      *    Step 1: Find day-of-week for November 1
           COMPUTE WS-NOV-1 =
               WS-INPUT-YEAR * 10000 + 1101
           COMPUTE WS-NOV-1-DOW =
               FUNCTION MOD(
                   FUNCTION INTEGER-OF-DATE(WS-NOV-1)
                   - 1, 7) + 1

      *    Step 2: Calculate first Thursday (DOW = 4)
           IF WS-NOV-1-DOW <= 4
               COMPUTE WS-FIRST-THU-DAY =
                   4 - WS-NOV-1-DOW + 1
           ELSE
               COMPUTE WS-FIRST-THU-DAY =
                   11 - WS-NOV-1-DOW + 1
           END-IF

      *    Step 3: Fourth Thursday = first + 21
           COMPUTE WS-THANKSGIVING-DAY =
               WS-FIRST-THU-DAY + 21
           COMPUTE WS-THANKSGIVING =
               WS-INPUT-YEAR * 10000
               + 1100 + WS-THANKSGIVING-DAY
           .

       COMPUTE-MEMORIAL-DAY.
      *    Last Monday in May for given year
      *    Step 1: Find day-of-week for May 31
           COMPUTE WS-MAY-31 =
               WS-INPUT-YEAR * 10000 + 0531
           COMPUTE WS-MAY-31-DOW =
               FUNCTION MOD(
                   FUNCTION INTEGER-OF-DATE(WS-MAY-31)
                   - 1, 7) + 1

      *    Step 2: Back up to Monday (DOW = 1)
           IF WS-MAY-31-DOW = 1
               MOVE WS-MAY-31 TO WS-MEMORIAL-DAY
           ELSE
               COMPUTE WS-MEMORIAL-DAY =
                   FUNCTION DATE-OF-INTEGER(
                       FUNCTION INTEGER-OF-DATE(WS-MAY-31)
                       - WS-MAY-31-DOW + 1)
           END-IF
           .

💡 Production Tip: Sarah Kim maintains a floating holiday calculator at MedClaim that generates the entire year's holiday table on January 1 (or at program initialization). The generated table is stored in a flat file so that all programs can load it without recomputing. This approach ensures consistency across all programs and allows operations staff to add ad hoc holidays (company closure for weather, etc.) by editing the file.

21.11 Date Processing in Batch Windows

Production batch systems have unique date processing requirements that stem from the timing of batch execution. Understanding these requirements is essential for any COBOL programmer working on batch systems.

The Business Date vs. System Date Problem

Batch programs that run near midnight face a fundamental problem: the system date may change during execution. A batch that starts at 11:45 PM on January 15 and runs until 12:30 AM on January 16 will see two different dates from FUNCTION CURRENT-DATE.

The standard solution is to establish a business date at the start of the batch and use it consistently:

       01  WS-BUSINESS-DATE    PIC 9(8).
       01  WS-SYSTEM-DATE      PIC 9(8).

       ESTABLISH-BUSINESS-DATE.
      *    Option 1: Accept business date as a parameter
           ACCEPT WS-BUSINESS-DATE FROM JCL-PARM
           IF WS-BUSINESS-DATE = SPACES
               OR WS-BUSINESS-DATE = ZEROES
      *        Option 2: Default to system date
               MOVE FUNCTION CURRENT-DATE(1:8)
                   TO WS-BUSINESS-DATE
           END-IF

      *    Validate the business date
           COMPUTE WS-DATE-TEST =
               FUNCTION TEST-DATE-YYYYMMDD(
                   WS-BUSINESS-DATE)
           IF WS-DATE-TEST NOT = ZERO
               DISPLAY "INVALID BUSINESS DATE: "
                   WS-BUSINESS-DATE
               MOVE 16 TO RETURN-CODE
               STOP RUN
           END-IF

           DISPLAY "BUSINESS DATE ESTABLISHED: "
               WS-BUSINESS-DATE
           .

⚠️ Critical Production Pattern: James Okafor's first rule of batch programming: "The business date is a parameter, not a system call. Every batch program at MedClaim accepts a business date through the JCL PARM or a control file. This allows us to rerun a batch for a previous date without changing system clocks, which is essential for error recovery and audit requirements."

Date Rollover and Rerun Handling

When a batch fails and must be rerun, the business date parameter ensures consistent results:

      *    In JCL:
      *    //STEP01 EXEC PGM=CLMAGING,PARM='20240315'
      *
      *    This allows:
      *    1. Normal run: PARM=today's date
      *    2. Rerun for yesterday: PARM=yesterday's date
      *    3. Month-end rerun: PARM=last day of month
      *    4. Audit recreation: PARM=any historical date

Batch Date Continuity Checking

Production batch systems verify that they are processing the correct date in sequence:

       01  WS-LAST-PROC-DATE  PIC 9(8).
       01  WS-EXPECTED-DATE   PIC 9(8).
       01  WS-DAY-GAP         PIC S9(3).

       CHECK-DATE-CONTINUITY.
      *    Read last successfully processed date from
      *    control file
           READ CONTROL-FILE INTO WS-LAST-PROC-DATE
      *    Compute expected date (next business day)
           MOVE WS-LAST-PROC-DATE TO WS-START-DATE
           MOVE 1 TO WS-BUS-DAYS-TO-ADD
           PERFORM ADD-BUSINESS-DAYS
           MOVE WS-RESULT-DATE TO WS-EXPECTED-DATE

      *    Compare with today's business date
           IF WS-BUSINESS-DATE NOT = WS-EXPECTED-DATE
               COMPUTE WS-DAY-GAP =
                   FUNCTION INTEGER-OF-DATE(
                       WS-BUSINESS-DATE)
                 - FUNCTION INTEGER-OF-DATE(
                       WS-EXPECTED-DATE)
               IF WS-DAY-GAP > 0
                   DISPLAY "WARNING: GAP OF " WS-DAY-GAP
                       " DAYS SINCE LAST PROCESSING"
                   DISPLAY "LAST PROCESSED: "
                       WS-LAST-PROC-DATE
                   DISPLAY "EXPECTED:       "
                       WS-EXPECTED-DATE
                   DISPLAY "TODAY:          "
                       WS-BUSINESS-DATE
      *            Depending on policy, this may be a
      *            warning or a fatal error
               ELSE
                   DISPLAY "NOTE: REPROCESSING DATE "
                       WS-BUSINESS-DATE
               END-IF
           END-IF
           .

This continuity check catches the common production problem of accidentally skipping a day's processing or running the same day twice. At GlobalBank, Maria Chen's team treats a gap of more than one business day as a fatal error requiring manual intervention: "Better to stop and investigate than to process three days' worth of transactions with potentially stale reference data."

End-of-Period Date Determination

Batch systems frequently need to determine whether the current business date is the last business day of a period (month, quarter, year) because additional processing runs on those dates:

       01  WS-PERIOD-FLAGS.
           05  WS-IS-MONTH-END    PIC X VALUE "N".
               88  IS-MONTH-END       VALUE "Y".
           05  WS-IS-QUARTER-END  PIC X VALUE "N".
               88  IS-QUARTER-END     VALUE "Y".
           05  WS-IS-YEAR-END     PIC X VALUE "N".
               88  IS-YEAR-END        VALUE "Y".

       DETERMINE-PERIOD-END.
           MOVE "N" TO WS-IS-MONTH-END
                        WS-IS-QUARTER-END
                        WS-IS-YEAR-END

      *    Get next business day
           MOVE WS-BUSINESS-DATE TO WS-START-DATE
           MOVE 1 TO WS-BUS-DAYS-TO-ADD
           PERFORM ADD-BUSINESS-DAYS
      *    WS-RESULT-DATE now has next business day

      *    If next business day is in a different month,
      *    today is month-end
           IF WS-RESULT-DATE(5:2) NOT =
              WS-BUSINESS-DATE(5:2)
               SET IS-MONTH-END TO TRUE

      *        Check quarter end (month 3, 6, 9, 12)
               EVALUATE WS-BUSINESS-DATE(5:2)
                   WHEN "03" WHEN "06"
                   WHEN "09" WHEN "12"
                       SET IS-QUARTER-END TO TRUE
               END-EVALUATE

      *        Check year end
               IF WS-BUSINESS-DATE(5:2) = "12"
                   SET IS-YEAR-END TO TRUE
               END-IF
           END-IF

           IF IS-MONTH-END
               DISPLAY "*** MONTH-END PROCESSING ***"
           END-IF
           IF IS-QUARTER-END
               DISPLAY "*** QUARTER-END PROCESSING ***"
           END-IF
           IF IS-YEAR-END
               DISPLAY "*** YEAR-END PROCESSING ***"
           END-IF
           .

Best Practice: Notice that this code does not hard-code "the last calendar day of the month." Instead, it checks whether the next business day falls in a different month. This correctly handles the case where the last calendar day (e.g., December 31) falls on a weekend — in that case, the last business day of the month might be December 29 (Friday), and that is when month-end processing should run.

21.12 Date Formatting and Display

Production reports require dates in various human-readable formats. While LE date services (CEEDATM) can format dates on z/OS, you can also build portable formatters using reference modification and month name tables.

Format: Month DD, YYYY

       01  WS-FORMATTED-DATE  PIC X(20).
       01  WS-FMT-PTR         PIC 9(3).

       FORMAT-WRITTEN-DATE.
      *    Input: WS-INPUT-DATE (YYYYMMDD)
           MOVE SPACES TO WS-FORMATTED-DATE
           MOVE 1 TO WS-FMT-PTR

           MOVE WS-INPUT-DATE(5:2) TO WS-INPUT-MONTH

      *    Month name
           MOVE FUNCTION TRIM(
               MONTH-NAME(WS-INPUT-MONTH) TRAILING)
               TO WS-FORMATTED-DATE(WS-FMT-PTR:)
           ADD FUNCTION LENGTH(
               FUNCTION TRIM(
                   MONTH-NAME(WS-INPUT-MONTH) TRAILING))
               TO WS-FMT-PTR

      *    Space
           MOVE " " TO WS-FORMATTED-DATE(WS-FMT-PTR:1)
           ADD 1 TO WS-FMT-PTR

      *    Day (without leading zero)
           IF WS-INPUT-DATE(7:1) = "0"
               MOVE WS-INPUT-DATE(8:1) TO
                   WS-FORMATTED-DATE(WS-FMT-PTR:1)
               ADD 1 TO WS-FMT-PTR
           ELSE
               MOVE WS-INPUT-DATE(7:2) TO
                   WS-FORMATTED-DATE(WS-FMT-PTR:2)
               ADD 2 TO WS-FMT-PTR
           END-IF

      *    ", "
           MOVE ", " TO WS-FORMATTED-DATE(WS-FMT-PTR:2)
           ADD 2 TO WS-FMT-PTR

      *    Year
           MOVE WS-INPUT-DATE(1:4) TO
               WS-FORMATTED-DATE(WS-FMT-PTR:4)

      *    Result: "June 15, 2024"
           .

Format: MM/DD/YYYY (US Format)

       FORMAT-US-DATE.
      *    Input: WS-INPUT-DATE (YYYYMMDD)
      *    Output: WS-US-FORMAT (MM/DD/YYYY)
           STRING WS-INPUT-DATE(5:2) "/"
                  WS-INPUT-DATE(7:2) "/"
                  WS-INPUT-DATE(1:4)
               DELIMITED BY SIZE
               INTO WS-US-FORMAT
           END-STRING
           .

Format: DD-Mon-YYYY (International)

       01  MONTH-ABBREV-VALS.
           05  FILLER PIC X(3) VALUE "Jan".
           05  FILLER PIC X(3) VALUE "Feb".
           05  FILLER PIC X(3) VALUE "Mar".
           05  FILLER PIC X(3) VALUE "Apr".
           05  FILLER PIC X(3) VALUE "May".
           05  FILLER PIC X(3) VALUE "Jun".
           05  FILLER PIC X(3) VALUE "Jul".
           05  FILLER PIC X(3) VALUE "Aug".
           05  FILLER PIC X(3) VALUE "Sep".
           05  FILLER PIC X(3) VALUE "Oct".
           05  FILLER PIC X(3) VALUE "Nov".
           05  FILLER PIC X(3) VALUE "Dec".
       01  MONTH-ABBREVS REDEFINES MONTH-ABBREV-VALS.
           05  MONTH-ABBR  PIC X(3) OCCURS 12 TIMES.

       FORMAT-INTL-DATE.
           MOVE WS-INPUT-DATE(5:2) TO WS-INPUT-MONTH
           STRING WS-INPUT-DATE(7:2) "-"
                  MONTH-ABBR(WS-INPUT-MONTH) "-"
                  WS-INPUT-DATE(1:4)
               DELIMITED BY SIZE
               INTO WS-INTL-FORMAT
           END-STRING
      *    Result: "15-Jun-2024"
           .

Elapsed Time Formatting

For batch processing reports, elapsed time is displayed in hours, minutes, and seconds:

       01  WS-ELAPSED-SECS    PIC 9(7).
       01  WS-ELAPSED-DISPLAY PIC X(12).
       01  WS-EL-HOURS        PIC 99.
       01  WS-EL-MINS         PIC 99.
       01  WS-EL-SECS         PIC 99.

       FORMAT-ELAPSED-TIME.
           COMPUTE WS-EL-HOURS =
               FUNCTION INTEGER-PART(
                   WS-ELAPSED-SECS / 3600)
           COMPUTE WS-EL-MINS =
               FUNCTION INTEGER-PART(
                   FUNCTION MOD(WS-ELAPSED-SECS, 3600)
                   / 60)
           COMPUTE WS-EL-SECS =
               FUNCTION MOD(WS-ELAPSED-SECS, 60)

           STRING WS-EL-HOURS ":" WS-EL-MINS
                  ":" WS-EL-SECS
               DELIMITED BY SIZE
               INTO WS-ELAPSED-DISPLAY
           END-STRING
      *    Result: "02:35:17"
           .

🧪 Try It Yourself: Date Format Library

Build a reusable date formatting library as a set of paragraphs that can be COPYed into any program. Include formatters for: 1. ISO format: 2024-06-15 2. US format: 06/15/2024 3. European format: 15/06/2024 4. Written format: June 15, 2024 5. Julian format: 2024-167 6. Compact format: 20240615

Accept a format code parameter and route to the correct formatter.

21.11 Date Ranges and Overlap Detection

Many business processes need to work with date ranges — policy effective periods, promotional windows, coverage dates. Detecting overlaps between ranges is a common and error-prone operation.

Range Overlap Logic

Two date ranges [start1, end1] and [start2, end2] overlap if and only if:

start1 <= end2 AND start2 <= end1

In COBOL:

       01  WS-RANGE-1.
           05  WS-R1-START    PIC 9(8).
           05  WS-R1-END      PIC 9(8).
       01  WS-RANGE-2.
           05  WS-R2-START    PIC 9(8).
           05  WS-R2-END      PIC 9(8).
       01  WS-OVERLAP-FLAG    PIC X(1).
           88  WS-RANGES-OVERLAP    VALUE "Y".
           88  WS-NO-OVERLAP        VALUE "N".

       CHECK-DATE-OVERLAP.
           IF WS-R1-START <= WS-R2-END
           AND WS-R2-START <= WS-R1-END
               SET WS-RANGES-OVERLAP TO TRUE
           ELSE
               SET WS-NO-OVERLAP TO TRUE
           END-IF
           .

Computing the Overlap Period

When ranges overlap, you may need to compute the overlapping period:

       COMPUTE-OVERLAP-PERIOD.
           PERFORM CHECK-DATE-OVERLAP
           IF WS-RANGES-OVERLAP
      *        Overlap start = later of the two starts
               IF WS-R1-START >= WS-R2-START
                   MOVE WS-R1-START
                       TO WS-OVERLAP-START
               ELSE
                   MOVE WS-R2-START
                       TO WS-OVERLAP-START
               END-IF

      *        Overlap end = earlier of the two ends
               IF WS-R1-END <= WS-R2-END
                   MOVE WS-R1-END TO WS-OVERLAP-END
               ELSE
                   MOVE WS-R2-END TO WS-OVERLAP-END
               END-IF

      *        Overlap days
               COMPUTE WS-OVERLAP-DAYS =
                   FUNCTION INTEGER-OF-DATE(
                       WS-OVERLAP-END)
                 - FUNCTION INTEGER-OF-DATE(
                       WS-OVERLAP-START) + 1
           ELSE
               MOVE ZERO TO WS-OVERLAP-DAYS
           END-IF
           .

📊 MedClaim Application: Date range overlap detection is critical for coordination of benefits (COB). When a patient has two insurance policies, MedClaim must determine whether the coverage periods overlap and, if so, which policy is primary for the date of service. James Okafor's adjudication engine performs this check on every claim with a COB indicator.

Date Range Validation

Validate that a date range makes logical sense:

       VALIDATE-DATE-RANGE.
           SET WS-RANGE-VALID TO TRUE

      *    Both dates must be valid
           MOVE WS-R1-START TO WS-INPUT-DATE
           PERFORM VALIDATE-DATE
           IF WS-DATE-INVALID
               SET WS-RANGE-INVALID TO TRUE
               MOVE "INVALID START DATE" TO WS-ERR-MSG
               EXIT PARAGRAPH
           END-IF

           MOVE WS-R1-END TO WS-INPUT-DATE
           PERFORM VALIDATE-DATE
           IF WS-DATE-INVALID
               SET WS-RANGE-INVALID TO TRUE
               MOVE "INVALID END DATE" TO WS-ERR-MSG
               EXIT PARAGRAPH
           END-IF

      *    End must be >= Start
           IF WS-R1-END < WS-R1-START
               SET WS-RANGE-INVALID TO TRUE
               MOVE "END DATE BEFORE START DATE"
                   TO WS-ERR-MSG
               EXIT PARAGRAPH
           END-IF

      *    Range should not exceed maximum span
           COMPUTE WS-RANGE-DAYS =
               FUNCTION INTEGER-OF-DATE(WS-R1-END)
             - FUNCTION INTEGER-OF-DATE(WS-R1-START)
           IF WS-RANGE-DAYS > 366
               SET WS-RANGE-WARNING TO TRUE
               MOVE "RANGE EXCEEDS 1 YEAR"
                   TO WS-WARN-MSG
           END-IF
           .

21.12 GlobalBank Case Study: Statement Period Calculations

GlobalBank generates monthly statements on different cycle dates for different account groups. The statement must cover exactly one month, handle month-end boundaries correctly, and include accurate transaction aging.

      *============================================================
      * GlobalBank Statement Period Calculator
      * Determines statement start/end dates based on
      * cycle day and accounts for month-end adjustments.
      *============================================================

       01  WS-CYCLE-DAY       PIC 99.
       01  WS-STMT-START      PIC 9(8).
       01  WS-STMT-END        PIC 9(8).
       01  WS-PREV-MONTH      PIC 9(2).
       01  WS-PREV-YEAR       PIC 9(4).

       COMPUTE-STATEMENT-PERIOD.
      *    Statement ends on cycle day of current month
      *    Statement starts on cycle day of previous month

      *    Current month end date
           MOVE WS-CD-YEAR TO WS-STMT-END(1:4)
           MOVE WS-CD-MONTH TO WS-STMT-END(5:2)

      *    Adjust cycle day for short months
           IF WS-CYCLE-DAY > MAX-DAYS(WS-CD-MONTH)
               MOVE MAX-DAYS(WS-CD-MONTH)
                   TO WS-STMT-END(7:2)
           ELSE
               MOVE WS-CYCLE-DAY TO WS-STMT-END(7:2)
           END-IF

      *    Previous month start date
           IF WS-CD-MONTH = 1
               MOVE 12 TO WS-PREV-MONTH
               COMPUTE WS-PREV-YEAR = WS-CD-YEAR - 1
           ELSE
               COMPUTE WS-PREV-MONTH =
                   WS-CD-MONTH - 1
               MOVE WS-CD-YEAR TO WS-PREV-YEAR
           END-IF

           MOVE WS-PREV-YEAR TO WS-STMT-START(1:4)
           MOVE WS-PREV-MONTH TO WS-STMT-START(5:2)

      *    Adjust for short previous month
           IF WS-CYCLE-DAY >
              MAX-DAYS(WS-PREV-MONTH)
               MOVE MAX-DAYS(WS-PREV-MONTH)
                   TO WS-STMT-START(7:2)
           ELSE
               MOVE WS-CYCLE-DAY
                   TO WS-STMT-START(7:2)
           END-IF

      *    Add 1 day to start (day after previous close)
           COMPUTE WS-STMT-START =
               FUNCTION DATE-OF-INTEGER(
                   FUNCTION INTEGER-OF-DATE(
                       WS-STMT-START) + 1)

           DISPLAY "STATEMENT PERIOD: "
                   WS-STMT-START " TO " WS-STMT-END
           .

📊 Production Reality: Derek Washington found 14 bugs in the legacy statement period calculation during his first review. Most involved February and month-end boundaries. The intrinsic function approach reduced the code from 120 lines to 40 lines and eliminated all month-boundary bugs.

21.11 MedClaim Case Study: Timely Filing Validation

Healthcare regulations require claims to be filed within a specific number of days from the date of service. The timely filing limit varies by payer (insurance company), typically ranging from 90 to 365 days. Claims filed after the deadline are denied.

       01  WS-TF-FIELDS.
           05  WS-TF-SERVICE-DATE  PIC 9(8).
           05  WS-TF-RECEIVED-DATE PIC 9(8).
           05  WS-TF-LIMIT-DAYS   PIC 9(3).
           05  WS-TF-ACTUAL-DAYS  PIC 9(5).
           05  WS-TF-DEADLINE     PIC 9(8).
           05  WS-TF-STATUS       PIC X(1).
               88  WS-TF-TIMELY       VALUE "T".
               88  WS-TF-UNTIMELY     VALUE "U".
               88  WS-TF-WARNING      VALUE "W".
               88  WS-TF-INVALID      VALUE "I".

       VALIDATE-TIMELY-FILING.
      *    Validate service date
           PERFORM VALIDATE-DATE
           IF WS-DATE-INVALID
               SET WS-TF-INVALID TO TRUE
               MOVE "INVALID SERVICE DATE"
                   TO WS-DENIAL-REASON
               EXIT PARAGRAPH
           END-IF

      *    Calculate days since service
           COMPUTE WS-TF-ACTUAL-DAYS =
               FUNCTION INTEGER-OF-DATE(
                   WS-TF-RECEIVED-DATE)
             - FUNCTION INTEGER-OF-DATE(
                   WS-TF-SERVICE-DATE)

      *    Validate not in the future
           IF WS-TF-ACTUAL-DAYS < ZERO
               SET WS-TF-INVALID TO TRUE
               MOVE "SERVICE DATE IN FUTURE"
                   TO WS-DENIAL-REASON
               EXIT PARAGRAPH
           END-IF

      *    Calculate filing deadline
           COMPUTE WS-TF-DEADLINE =
               FUNCTION DATE-OF-INTEGER(
                   FUNCTION INTEGER-OF-DATE(
                       WS-TF-SERVICE-DATE)
                   + WS-TF-LIMIT-DAYS)

      *    Determine status
           EVALUATE TRUE
               WHEN WS-TF-ACTUAL-DAYS <= WS-TF-LIMIT-DAYS
                   SET WS-TF-TIMELY TO TRUE
               WHEN WS-TF-ACTUAL-DAYS <=
                    WS-TF-LIMIT-DAYS + 30
                   SET WS-TF-WARNING TO TRUE
                   MOVE "APPROACHING TIMELY FILING LIMIT"
                       TO WS-WARNING-MSG
               WHEN OTHER
                   SET WS-TF-UNTIMELY TO TRUE
                   MOVE "TIMELY FILING LIMIT EXCEEDED"
                       TO WS-DENIAL-REASON
           END-EVALUATE

           DISPLAY "SERVICE DATE: " WS-TF-SERVICE-DATE
           DISPLAY "RECEIVED:     " WS-TF-RECEIVED-DATE
           DISPLAY "DAYS ELAPSED: " WS-TF-ACTUAL-DAYS
           DISPLAY "DEADLINE:     " WS-TF-DEADLINE
           DISPLAY "STATUS:       " WS-TF-STATUS
           .

MedClaim Aging Report

       PRODUCE-AGING-REPORT.
           INITIALIZE WS-AGING-BUCKETS
           INITIALIZE WS-AGING-COUNTS
           MOVE FUNCTION CURRENT-DATE(1:8) TO WS-TODAY

           PERFORM UNTIL WS-CLAIM-EOF
               COMPUTE WS-AGE-DAYS =
                   FUNCTION INTEGER-OF-DATE(WS-TODAY)
                 - FUNCTION INTEGER-OF-DATE(
                       WS-CLAIM-SERVICE-DATE)
               MOVE WS-CLAIM-AMOUNT TO WS-ITEM-AMOUNT
               PERFORM ASSIGN-AGING-BUCKET

               READ CLAIM-FILE INTO WS-CLAIM-REC
                   AT END SET WS-CLAIM-EOF TO TRUE
               END-READ
           END-PERFORM

           DISPLAY "CLAIM AGING REPORT"
           DISPLAY "===================="
           DISPLAY "CURRENT (0-30):   "
                   WS-COUNT-CURRENT " CLAIMS  "
                   WS-BUCKET-CURRENT
           DISPLAY "31-60 DAYS:       "
                   WS-COUNT-30 " CLAIMS  "
                   WS-BUCKET-30
           DISPLAY "61-90 DAYS:       "
                   WS-COUNT-60 " CLAIMS  "
                   WS-BUCKET-60
           DISPLAY "91-120 DAYS:      "
                   WS-COUNT-90 " CLAIMS  "
                   WS-BUCKET-90
           DISPLAY "OVER 120 DAYS:    "
                   WS-COUNT-OVER-90 " CLAIMS  "
                   WS-BUCKET-OVER-90
           .

21.12 Y2K Historical Timeline

Understanding Y2K's timeline helps appreciate the institutional discipline that date handling requires:

Year Event
1958 COBOL designed with 2-digit year fields (storage was $1M per megabyte)
1978 First papers published warning about the "Year 2000 problem"
1985 COBOL-85 standard does not address the 2-digit year issue
1989 Intrinsic functions added to COBOL-85 (amendment), including date functions with 4-digit years
1993 Peter de Jager publishes "Doomsday 2000" in ComputerWorld, raising mainstream awareness
1996 Major organizations begin Y2K remediation projects
1997 COBOL programmers come out of retirement; rates reach $150-200/hour
1998 Most critical systems remediated; testing begins
1999 Final testing and certification; contingency plans prepared
2000-01-01 The millennium arrives with minimal failures, validating the $300B+ remediation
2002 COBOL 2002 standard formalizes 4-digit year requirements

🔵 Theme: Legacy != Obsolete. Y2K demonstrated that COBOL systems are critical infrastructure. The successful remediation proved that COBOL code can be systematically analyzed, modified, and tested at industrial scale. The techniques developed during Y2K — automated code scanning, date field identification, regression testing — remain in use today for modernization projects.

21.13 The Student Mainframe Lab

🧪 Try It Yourself: Date Utility Suite

Build a comprehensive date utility program with the following functions: 1. Date Validator: Accept a date string and validate it completely (numeric, range, leap year) 2. Date Calculator: Accept two dates and compute the difference in days, weeks, and months 3. Business Day Calculator: Accept a start date and number of business days; compute the result date (excluding weekends and a hardcoded holiday table) 4. Aging Calculator: Accept a date and compute which aging bucket it falls into (current, 30, 60, 90, 120+) 5. Date Formatter: Accept a YYYYMMDD date and display it in 5 different formats (ISO, US, European, written, Julian)

Make each function a separate paragraph callable from a menu.

🧪 Try It Yourself: Y2K Simulator

Write a program that simulates the Y2K problem: 1. Store 10 dates using 2-digit years (PIC 99 for year) 2. Attempt to sort them and observe the incorrect ordering 3. Implement the windowing fix (pivot year = 50) 4. Implement the expansion fix (convert to 4-digit years) 5. Compare the results of all three approaches

This exercise demonstrates WHY Y2K was a problem and HOW each fix addresses it.

21.14 Complete Worked Example: Date Processing Utility Subprogram

In production environments, date processing logic should be centralized in a callable subprogram. This avoids the inconsistency problems described in the MedClaim case study, where the adjudication system and aging report used different date calculations. Here is a production-quality date utility subprogram that can be called from any program in the system.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. DTUTIL.
      *============================================================
      * Date Processing Utility Subprogram
      * Centralized date operations for validation,
      * arithmetic, aging, and day-of-week calculation.
      *
      * CALL 'DTUTIL' USING DU-FUNCTION DU-INPUT
      *                     DU-OUTPUT DU-RC
      *============================================================

       DATA DIVISION.
       WORKING-STORAGE SECTION.

       01  WS-TODAY            PIC 9(8).
       01  WS-TODAY-INT        PIC 9(7).
       01  WS-INIT-FLAG        PIC X VALUE "N".
           88  WS-IS-INIT         VALUE "Y".

       01  DIM-VALUES.
           05  FILLER PIC 99 VALUE 31.
           05  FILLER PIC 99 VALUE 28.
           05  FILLER PIC 99 VALUE 31.
           05  FILLER PIC 99 VALUE 30.
           05  FILLER PIC 99 VALUE 31.
           05  FILLER PIC 99 VALUE 30.
           05  FILLER PIC 99 VALUE 31.
           05  FILLER PIC 99 VALUE 31.
           05  FILLER PIC 99 VALUE 30.
           05  FILLER PIC 99 VALUE 31.
           05  FILLER PIC 99 VALUE 30.
           05  FILLER PIC 99 VALUE 31.
       01  DIM-TABLE REDEFINES DIM-VALUES.
           05  DAYS-IN-MONTH PIC 99 OCCURS 12 TIMES.

       01  WS-W-YEAR           PIC 9(4).
       01  WS-W-MONTH          PIC 9(2).
       01  WS-W-DAY            PIC 9(2).
       01  WS-W-MAX-DAY        PIC 99.
       01  WS-LEAP-FL          PIC X.
           88  WS-LEAP            VALUE "Y".

       LINKAGE SECTION.
       01  LS-FUNC             PIC X(4).
      *    VALD = Validate   DIFF = Days between
      *    ADDP = Add days   AGEM = Age from today
      *    WDAY = Day of week (1=Mon..7=Sun)

       01  LS-INPUT.
           05  LS-IN-DATE-1    PIC 9(8).
           05  LS-IN-DATE-2    PIC 9(8).
           05  LS-IN-DAYS      PIC S9(5).

       01  LS-OUTPUT.
           05  LS-OUT-DATE     PIC 9(8).
           05  LS-OUT-DAYS     PIC S9(5).
           05  LS-OUT-DOW      PIC 9.

       01  LS-RC               PIC S9(4) COMP.
      *    0=OK, 8=Invalid, 12=Bad function

       PROCEDURE DIVISION USING LS-FUNC LS-INPUT
                                LS-OUTPUT LS-RC.
       0000-MAIN.
           MOVE 0 TO LS-RC
           IF NOT WS-IS-INIT
               MOVE FUNCTION CURRENT-DATE(1:8)
                   TO WS-TODAY
               COMPUTE WS-TODAY-INT =
                   FUNCTION INTEGER-OF-DATE(WS-TODAY)
               MOVE "Y" TO WS-INIT-FLAG
           END-IF

           EVALUATE LS-FUNC
               WHEN "VALD"  PERFORM 1000-VALIDATE
               WHEN "DIFF"  PERFORM 2000-DIFF
               WHEN "ADDP"  PERFORM 3000-ADD
               WHEN "AGEM"  PERFORM 4000-AGING
               WHEN "WDAY"  PERFORM 5000-DOW
               WHEN OTHER   MOVE 12 TO LS-RC
           END-EVALUATE
           GOBACK.

       1000-VALIDATE.
           IF LS-IN-DATE-1 NOT NUMERIC
               MOVE 8 TO LS-RC
               EXIT PARAGRAPH
           END-IF
           MOVE LS-IN-DATE-1(1:4) TO WS-W-YEAR
           MOVE LS-IN-DATE-1(5:2) TO WS-W-MONTH
           MOVE LS-IN-DATE-1(7:2) TO WS-W-DAY
           IF WS-W-YEAR < 1900 OR > 2099
               MOVE 8 TO LS-RC  EXIT PARAGRAPH
           END-IF
           IF WS-W-MONTH < 01 OR > 12
               MOVE 8 TO LS-RC  EXIT PARAGRAPH
           END-IF
           MOVE DAYS-IN-MONTH(WS-W-MONTH)
               TO WS-W-MAX-DAY
           IF WS-W-MONTH = 2
               PERFORM 8000-LEAP-CHECK
               IF WS-LEAP MOVE 29 TO WS-W-MAX-DAY
               END-IF
           END-IF
           IF WS-W-DAY < 01 OR > WS-W-MAX-DAY
               MOVE 8 TO LS-RC
           END-IF.

       2000-DIFF.
           COMPUTE LS-OUT-DAYS =
               FUNCTION INTEGER-OF-DATE(LS-IN-DATE-2)
             - FUNCTION INTEGER-OF-DATE(LS-IN-DATE-1).

       3000-ADD.
           COMPUTE LS-OUT-DATE =
               FUNCTION DATE-OF-INTEGER(
                   FUNCTION INTEGER-OF-DATE(
                       LS-IN-DATE-1) + LS-IN-DAYS).

       4000-AGING.
           COMPUTE LS-OUT-DAYS =
               WS-TODAY-INT -
               FUNCTION INTEGER-OF-DATE(LS-IN-DATE-1).

       5000-DOW.
           COMPUTE LS-OUT-DOW =
               FUNCTION MOD(
                   FUNCTION INTEGER-OF-DATE(
                       LS-IN-DATE-1) - 1, 7) + 1.

       8000-LEAP-CHECK.
           MOVE "N" TO WS-LEAP-FL
           IF FUNCTION MOD(WS-W-YEAR, 4) = 0
               MOVE "Y" TO WS-LEAP-FL
               IF FUNCTION MOD(WS-W-YEAR, 100) = 0
                   MOVE "N" TO WS-LEAP-FL
                   IF FUNCTION MOD(WS-W-YEAR, 400) = 0
                       MOVE "Y" TO WS-LEAP-FL
                   END-IF
               END-IF
           END-IF.

Calling the Utility

Any program can use the centralized date logic:

*> In the calling program:
01  WS-DU-FUNC    PIC X(4).
01  WS-DU-INPUT.
    05  WS-DU-IN-1   PIC 9(8).
    05  WS-DU-IN-2   PIC 9(8).
    05  WS-DU-IN-DAYS PIC S9(5).
01  WS-DU-OUTPUT.
    05  WS-DU-OUT-DATE PIC 9(8).
    05  WS-DU-OUT-DAYS PIC S9(5).
    05  WS-DU-OUT-DOW  PIC 9.
01  WS-DU-RC       PIC S9(4) COMP.

*> Validate a date
    MOVE "VALD" TO WS-DU-FUNC
    MOVE 20240229 TO WS-DU-IN-1
    CALL "DTUTIL" USING WS-DU-FUNC WS-DU-INPUT
                        WS-DU-OUTPUT WS-DU-RC
    IF WS-DU-RC = 0
        DISPLAY "DATE IS VALID"
    END-IF

*> Compute days between two dates
    MOVE "DIFF" TO WS-DU-FUNC
    MOVE 20240101 TO WS-DU-IN-1
    MOVE 20240615 TO WS-DU-IN-2
    CALL "DTUTIL" USING WS-DU-FUNC WS-DU-INPUT
                        WS-DU-OUTPUT WS-DU-RC
    DISPLAY "DAYS BETWEEN: " WS-DU-OUT-DAYS

*> Compute aging
    MOVE "AGEM" TO WS-DU-FUNC
    MOVE 20240301 TO WS-DU-IN-1
    CALL "DTUTIL" USING WS-DU-FUNC WS-DU-INPUT
                        WS-DU-OUTPUT WS-DU-RC
    DISPLAY "AGE IN DAYS: " WS-DU-OUT-DAYS

This subprogram pattern ensures that every program in the system uses identical date logic. When a date-related bug is found, it is fixed in one place (DTUTIL) and all programs benefit immediately. The subprogram caches the "today" date on its first call, ensuring consistent aging calculations throughout a batch run even if the batch runs past midnight.

Best Practice: James Okafor's rule: "Every COBOL shop should have exactly one date utility subprogram, used by every program. No exceptions. When I see date arithmetic inline in a program, I know I am looking at a future bug."

21.16 Date Processing Testing Strategies

Date processing code is notoriously difficult to test because many bugs only manifest at specific calendar boundaries. A comprehensive testing strategy must deliberately target these boundaries.

Critical Test Dates

Every date processing program should be tested with the following categories of dates:

      *    Test Category 1: Leap year boundaries
      *    20240229 — Valid (2024 is a leap year)
      *    20230229 — Invalid (2023 is not a leap year)
      *    20000229 — Valid (divisible by 400)
      *    19000229 — Invalid (divisible by 100, not 400)

      *    Test Category 2: Month boundaries
      *    20240131 — January 31 (next day is February)
      *    20240228 — Feb 28 in non-leap year
      *    20240229 — Feb 29 in leap year
      *    20240331 — March 31 (next day is April 1)
      *    20240430 — April 30 (30-day month end)

      *    Test Category 3: Year boundaries
      *    20231231 — Last day of year
      *    20240101 — First day of year

      *    Test Category 4: Edge cases
      *    16010101 — COBOL epoch (earliest valid date)
      *    99991231 — Latest valid date
      *    00000000 — Zero date (invalid)
      *    99999999 — Maximum value (invalid)

⚠️ The February 29 Test: Tomás Rivera requires every MedClaim date program to be tested with February 29 of both leap and non-leap years. "In my career, I have seen at least a dozen production bugs that only appeared on February 29. One of them went undetected for four years — the exact length of the leap year cycle — because no one thought to test with that date."

Automated Date Boundary Testing

Build a test harness that exercises your date routines across all boundary conditions:

       TEST-DATE-BOUNDARIES.
      *    Test leap year detection
           MOVE 20240101 TO WS-TEST-DATE
           PERFORM TEST-ONE-DATE
           MOVE 20240229 TO WS-TEST-DATE
           PERFORM TEST-ONE-DATE
           MOVE 20230229 TO WS-TEST-DATE
           PERFORM TEST-ONE-DATE-EXPECT-INVALID
           MOVE 20000229 TO WS-TEST-DATE
           PERFORM TEST-ONE-DATE
           MOVE 19000229 TO WS-TEST-DATE
           PERFORM TEST-ONE-DATE-EXPECT-INVALID

      *    Test month-end transitions
           PERFORM VARYING WS-TEST-MONTH FROM 1 BY 1
               UNTIL WS-TEST-MONTH > 12
      *        Test last day of each month
               MOVE WS-MONTH-END-DAY(WS-TEST-MONTH)
                   TO WS-TEST-DAY
               COMPUTE WS-TEST-DATE =
                   20240000 + WS-TEST-MONTH * 100
                   + WS-TEST-DAY
               PERFORM TEST-ONE-DATE
      *        Test day after last day (invalid for 28/30
      *        day months)
               ADD 1 TO WS-TEST-DAY
               COMPUTE WS-TEST-DATE =
                   20240000 + WS-TEST-MONTH * 100
                   + WS-TEST-DAY
               PERFORM TEST-ONE-DATE-CHECK-VALIDITY
           END-PERFORM

      *    Test date arithmetic across year boundary
           MOVE 20231231 TO WS-START-DATE
           MOVE 20240101 TO WS-END-DATE
           PERFORM COUNT-BUSINESS-DAYS
           IF WS-BUS-DAY-COUNT NOT = ZERO
               DISPLAY "YEAR BOUNDARY TEST: PASS"
           END-IF

      *    Test large date spans
           MOVE 20200101 TO WS-START-DATE
           MOVE 20240101 TO WS-END-DATE
           PERFORM COUNT-BUSINESS-DAYS
           DISPLAY "4 YEAR SPAN: " WS-BUS-DAY-COUNT
                   " BUSINESS DAYS"
           .

Best Practice: Maria Chen's GlobalBank team runs date boundary tests as part of every program's regression suite. The test data file contains 200+ date combinations that cover every known edge case. New developers are required to add test cases for any date bug they discover.

21.17 Common Date Processing Mistakes

Mistake 1: Not Validating Dates

*> DANGEROUS - no validation
COMPUTE WS-DAYS =
    FUNCTION INTEGER-OF-DATE(WS-INPUT-DATE)
*> If WS-INPUT-DATE is 20240231 (Feb 31), this abends!

Mistake 2: Hardcoding Year Assumptions

*> WRONG - breaks after 2099
IF WS-YEAR(1:2) = "20"
    PERFORM PROCESS-CURRENT-CENTURY

Mistake 3: Ignoring Timezone in Batch

*> RISKY - near midnight, date may differ from business date
MOVE FUNCTION CURRENT-DATE(1:8) TO WS-BUSINESS-DATE
*> If batch runs at 11:55 PM EST, some transactions may
*> be dated tomorrow in UTC

Mistake 4: Subtracting Dates Directly

*> WRONG - does not account for different month lengths
COMPUTE WS-DAYS = WS-DATE-2 - WS-DATE-1
*> 20240301 - 20240228 = 73, not 2!

*> CORRECT
COMPUTE WS-DAYS =
    FUNCTION INTEGER-OF-DATE(WS-DATE-2)
  - FUNCTION INTEGER-OF-DATE(WS-DATE-1)

Mistake 5: Assuming 30 Days per Month

*> WRONG for financial calculations
COMPUTE WS-MONTHS = WS-DAYS / 30

*> Correct approach: compare year/month components

21.15 Chapter Summary

Date and time processing requires disciplined attention to detail that reflects COBOL's core strength: reliability in critical business systems. In this chapter, you have learned:

  • Y2K was real, expensive, and instructive. The $300+ billion remediation effort proved both COBOL's criticality and the importance of careful date handling. The lessons — always use 4-digit years, always validate dates, always test boundary conditions — remain essential today.
  • COBOL supports multiple date formats: Gregorian (YYYYMMDD), Julian (YYYYDDD), packed dates, integer dates, and timestamps. Choose the right format for each use case.
  • FUNCTION CURRENT-DATE provides the current date, time, and GMT offset in a 21-character string. It is the starting point for most date operations.
  • INTEGER-OF-DATE and DATE-OF-INTEGER enable date arithmetic by converting between calendar dates and integer day numbers. Never subtract calendar dates directly.
  • Date validation is mandatory. Use comprehensive checks (numeric, range, month/day, leap year) or compiler-specific functions like TEST-DATE-YYYYMMDD.
  • Language Environment date services provide additional formatting and conversion capabilities on z/OS systems.
  • Aging calculations use date subtraction to categorize items into buckets (current, 30, 60, 90, 120+ days).
  • Timezone handling uses the GMT offset from CURRENT-DATE, with awareness of day-boundary crossings.
  • Business day calculations combine day-of-week determination (via INTEGER-OF-DATE and MOD) with holiday table lookups.
  • Quarter and fiscal year calculations use month-based arithmetic with adjustments for non-calendar fiscal years.

Every production COBOL system processes dates. Getting date handling right is not glamorous work, but it is essential work — the kind of work that, done well, is invisible, and done poorly, makes headlines.


"The Y2K crisis taught an entire generation of programmers that dates are not just data — they are the heartbeat of every business system. Treat them with respect." — Maria Chen, Senior Developer, GlobalBank